Radiative cooling and heating#
Further reading
More information on cooling and heating in RAMSES can be found in Rosdahl’s PhD thesis, or in the RAMSES-RT paper. A classical paper on the topic is Katz, Weinberg, & Hernquist (1996).
In most astrophysical contexts, gas dissipates thermal energy through collisional processes, and this energy is carried away by radiation. Gas may also gain energy from radiation, for example through photo-ionisations. In RAMSES, these radiative cooling and heating terms are accounted for in the energy conservation equation with the net cooling rate \(\Lambda\):
Here, \(t\) is time, \(\boldsymbol{u}\) is the gas bulk velocity, \(\phi\) is the gravitational potential, \(E = \frac{1}{2}\rho u^2 + e\) is the gas total energy, \(e\) is the internal energy, and \(P = (\gamma -1)e\), with \(\gamma\) the ratio of specific heats.
The net cooling rate \(\Lambda\) is a sum over many microscopic processes, involving collisions between ions and electrons, photo-ionisations, etc. In principle, it is thus a function of the temperature and density of the gas, of its detailed chemical composition and ionisation state, and of the local radiation field. The different cooling models implemented in RAMSES mostly differ on how they approximate this function.
The following approximations are available in RAMSES:
ISM cooling: a simplified prescription for cold, dense interstellar medium conditions.
Equilibrium cooling with a UVB (default): assumes photo-ionisation equilibrium with a redshift-dependent UV background.
Equilibrium cooling with Grackle: same physical approximation using the external Grackle library.
Non-equilibrium cooling without RHD: tracks ion fractions explicitly with a homogeneous UVB.
Non-equilibrium cooling with RHD: full radiative transfer coupled to chemistry of H and He.
Below, we will detail the implementation of the default model (equilibrium cooling) and of the non-equilibrium cooling (with a UVB) as illustrative examples of all cooling models implementations.
Step by step default cooling model#
initialisations#
Some models require initialisations, and these are done in init_time
(in amr/init_time.f90) which is called at the beginning of a
simulation (see code snippet below). For the default cooling model, this involves a call to
set_model in hydro/cooling_module.f90. The set_model routine
does two things: a) it initialises UV background parameters, and b), for
cosmological simulations, it computes the (homogeneous) temperature of
the Universe at the starting redshift of the simulation (typically read
from the cosmological initial conditions, but possible to override with
the aexp_ini namelist parameter).
subroutine adaptive_loop
...
call init_time
...
if(cooling) call set_table(dble(aexp))
...
do ! Main time loop
...
call amr_step(levelmin,1)
...
end do
...
end subroutine adaptive_loop
A second initialisation here is a call to set_table() (from hydro/cooling_module.f90) before entering the
main time-evolution loop of RAMSES (see above). This is a first computation of the cooling
and heating rates tables.
Indeed, the default cooling module assumes photoionization equilibrium (PIE) with a redshift-dependent UVB. In such conditions, the abundances of H and He ions are direct functions of temperature, density, and redshift only. In turn, the cooling and heating rates are also reduced to being functions only of temperature, density, redshift and metallicity, so that the cooling function becomes \(\Lambda \simeq \Lambda(T,n_H, Z, z)\). The default cooling method simplifies things further by assuming a linear dependence with metallicity so that the cooling function becomes \(\CH = (\Heat + \Cool + Z/\Zsun \, \CoolZ)\, \nh^2\), where
\(\Heat(\Tmu,\nh)\) is the heating rate from the UV background at the current redshift.
\(\Cool(\Tmu,\nh)\) is the cooling rate due to H and He (and electrons) assuming PIE with the UVB at the current redshift.
\(\CoolZ(\Tmu,\nh)\) is the cooling rate due to metals, at solar metallicity, and assuming PIE with the UVB at the current redshift.
With only two dimensions to the problem, it is much more efficient to pre-compute these rates and use linear interpolations than to calculate them on-the-fly
(exponents, powers).
The call to set_table computes and saves into tables these rates. As we’ll see below, this is done at each coarse timestep to track the evolution of the UVB.
Filling in the rate tables with set_table#
set_table first computes the PIE ionisation state for H and He, with a simple iterative process that involves equating the
rates of photo-ionization, collisional ionization and recombination
(this is done in the cmp_chem_eq routine in
hydro/cooling_module.f90). Then the cooling and heating rates are computed and stored in tables
for a range of \((\Tmu,\nh)\)-bins, where \(T_\mu=T/\mu\)
and \(\mu\) is the mean particle mass in units of \(m_H\), \(\nh=X\rho/m_H\) is
hydrogen number density, and \(X\) is the hydrogen mass fraction in
the gas (a global constant, typically set to the value of \(0.76\)).
The abundances table:
The abundances \(n_i\), of each of the 6 primordial species (e, HI, HII, HeI, HeII, HeIII) are computed and stored for a range of (\(\Tmu, \nh\)) values. Here, rates are used for all the possible interactions involving these species - these are functions of photoionization rates \(\Gamma_i\), \(T\) and \(n_i\), and amount to a closed set of equations that are converged iteratively to an equilibrium solution, such that the creation rate equals the destruction rate for each species. The species abundances also give the value of \(\mu\) per (\(\Tmu, \nh\))-bin, which can be retrieved by
where \(Y=1-X\) is the helium mass fraction, \(\xhii \equiv \nhii/\nh\), \(\xheii \equiv \nheii/\nhe\) and \(\xheiii \equiv \nheiii/\nhe\). The tabulation of the abundances also provides a direct mapping between \(\Tmu\) and \(T\) which is useful in generating the rest of the tables.
Note that these tables are written to each ramses output directory, allowing the user to easily extract the equilibrium ionisation fractions and \(\mu\) in postprocessing (by performing the same kind of interpolation for every cell as is done in RAMSES).
The cooling rates table:
Given the abundances, it is then a straightforward matter to calculate and tabulate \(\Cool(\Tmu,\nh)\) – the cooling rate is a sum of bremsstrahlung, collisional excitation, collisional ionization, recombination, dielectric recombination and Compton cooling rates, all fitted analytic functions of temperature and abundances, and fetched from various sources in the literature (e.g. Cen, 1992).
The heating rates table:
It is also straightforward to tabulate \(\Heat(\Tmu,\nh)\). Each bin contains
where the sum is over the primordial ion species, and \(\Heat_i\) are photoheating rates for the individual species (\(\nhi\), \(\nhei\), \(\nheii\), and e).
The metal-cooling-contributions table:
RAMSES keeps a hard-coded table of a precomputed metal-cooling rate contribution, \(\Cool_{Z}^{CIE}(T)\), which is the difference between zero metallicity and solar metallicity cooling rates calculated assuming collisional ionization equilibrium (CIE, i.e. chemical equilibrium in zero ionizing radiation), with the CLOUDY software package (Ferland et al., 1998). That is,
These rates are computed for a photoionization-free environment so they don’t depend on gas density. Using this, the photoionization equilibrium (PIE) metal-cooling rates are approximated and tabulated as
where \(f\) is a dimensionless analytic function that corrects for density \(\nh\) and UV background photoionization at redshift \(z\).
Implementation:
For the default cooling model, the implementation of these computations is all in hydro/cooling_module.f90,
with the following structure.
subroutine set_table(aexp)
... ! define boundaries of tables in terms of nH and T
call cmp_table(nH_min,nH_max,T2_min,T2_max,nbin_n,nbin_T,aexp) ! compute the tables.
end subroutine set_table
subroutine cmp_table(nH_min,nH_max,T2_min,T2_max,nbin_n,nbin_T,aexp)
! Compute radiative ionization and heating rates
call set_rates(t_rad_spec,h_rad_spec,aexp)
! Create the table
do i_n = myid,nbin_n,ncpu
call iterate(i_n,t_rad_spec,h_rad_spec,nbin_T,aexp) ! compute cooling/heating for a range of temperatures
end do
end subroutine cmp_table
subroutine iterate(i_n,t_rad_spec,h_rad_spec,nbin_T,aexp)
nH = ... ! define value of nH from i_n
do i_T = 1,nbin_T
! Compute cooling, heating and mean molecular weight
T2=10d0**table%T2(i_T) ! define the temperature of current bin
call cmp_cooling(T2,nH ...) ! compute equilibrium cooling solution
...
! Compute cooling and heating derivatives
T2_eps=10d0**(table%T2(i_T)+0.01d0) ! define slightly larger temperature ("slightly = 0.01")
call cmp_cooling(T2_eps,nH, ...)
table%cool_prime(i_n,i_T)=(log10(cool_tot_eps)-log10(cool_tot))/0.01d0 ! evaluate the derivative and store in _prime tables.
...
! Compute metal contribution for solar metallicity
call cmp_metals(T2,nH,mu,metal_tot,metal_prime,aexp)
...
end do
end subroutine iterate
subroutine cmp_cooling(T2,nH,t_rad_spec,h_rad_spec,cool_tot,heat_tot,cool_com,heat_com,mu_out,aexp,n_spec)
! Iteration to find mu (rates depend on T, but we only know T/mu)
do while (error is too large)
T = ... ! get T from T/mu and a guessed value of mu
call cmp_chem_eq(T,nH,t_rad_spec,n_spec,n_TOT,mu) ! get equilibrium solution at fixed T, including mu.
...
end do
! Get equilibrium abundances
n_E = n_spec(1) ! electrons
n_HI = n_spec(2) ! H
n_HII = n_spec(3) ! H+
...
! Bremstrahlung
cb1 = cool_bre(HI ,T)*n_E*n_HII /nH**2
...
! Ionization cooling
ci1 = cool_ion(HI ,T)*n_E*n_HI /nH**2
... etc ...
end subroutine cmp_cooling
Operator-splitting and temperature update#
For all cooling models, RAMSES uses operator splitting to solve the Euler equations in two
steps. In a first step, gravity is computed, and the gas is advected,
basically solving the Euler equations with \(\Lambda = 0\). In a
second thermo-chemistry step, the heating and cooling terms are
computed, and the internal energy is updated with \(\dot{e} = \Lambda\).
This strategy is shown in the code snippet below taken from the main code loop amr_step.
RAMSES first computes the hydrodynamics and updates uold with a call to godunov_fine.
Then, RAMSES computes cooling on the updated state uold with a call to cooling_fine (more details below).
subroutine amr_step
...
call godunov_fine ! do the hydro and update uold.
...
call cooling_fine ! do the cooling and update uold.
...
end subroutine amr_step
The thermo-chemistry involves the interaction of radiation and matter, and the implementation differs if the code is used in radiation-hydrodynamics (RHD) mode or in hydro (HD) mode illustrated above. With RHD, the radiation is transfered accross the grid and the thermo-chemistry is computed on the fly.
In the general case, RAMSES implements a semi-implicit scheme to evolve the temperature due to cooling during a timestep. With \(\rho\) the mass density, \(\gamma\) the ratio of specific heats (usually given the value of \(5/3\) in RAMSES, corresponding to monatomic gas), \(m_H\) the proton mass, \(\kb\) the Boltzmann constant, and \(\mu m_H\) the average particle mass, one can write the internal energy as
It is clear here that after advection, when the density (and metallicity) is fixed, evolving the internal energy is equivalent to evolving the ratio \(\Tmu \equiv T/\mu\). This is what is done in RAMSES, by solving the equation
Starting at time \(t\) with temperature \(\Tmu^{t}\), we compute the evolved temperature \(\Tmu^{t+\Delta t}\) with a semi-implicit Euler formulation (See Press et al., 1992):
where \(K=\eTconv\) is the conversion factor between \(e\) and \(\Tmu\) and \(\CHp \equiv \frac{\partial \Lambda}{\partial \Tmu}\) can be estimated by finite-differencing the rate tables.
The thermochemistry is called in amr_step with a call to
cooling_fine (in hydro/cooling_fine.f90), after the gravity
source term and hydro advection. This evolves the temperature of all
cells at the given level, over the current hydrodynamical timestep
length, \(\dthyd\).
Details of the cooling_fine subroutine.#
In cooling_fine(), cells at the given level are collected into
vectors of size NVECTOR and then each vector of cells is processed
in coolfine1(). This is a generic operation which is useful for
vectorisation efficiency, and we illustrate it in the snippet below.
Notice that at the end of cooling_fine, we again call set_table
if we’re at a coarse level (ilevel==levelmin) to prepare the next timestep.
subroutine cooling_fine(ilevel)
...
! Operator splitting step for cooling source term
! by vector sweeps
ncache=active(ilevel)%ngrid
do igrid=1,ncache,nvector
ngrid=MIN(nvector,ncache-igrid+1)
do i=1,ngrid
ind_grid(i)=active(ilevel)%igrid(igrid+i-1)
end do
call coolfine1(ind_grid,ngrid,ilevel)
end do
if((cooling.and..not.neq_chem.and..not.cooling_ism).and.ilevel==levelmin.and.cosmo)then
call set_table(dble(aexp))
endif
end subroutine cooling_fine
The coolfine1 subroutine basically collects the density,
temperature, and metallity for a batch of cells, in CGS units, and then calls
solve_cooling() for those cells, which returns the (positive or
negative) change in the temperature, in Kelvin, over \(\dthyd\).
This is then converted back a change in internal energy densities, in
code units, and then uold is updated accordingly for each of the
cells.
coolfine1 follows these steps in order:
Unit conversion — calls
units()to obtain scale factors from code units to CGS.loop over cells from ind_grid list
2.1. Select leaf cells — only cells with no children (
son == 0) are processed. An index arrayind_leafis built; the routine returns immediately ifnleaf == 0.2.2. Extract physical quantities — reads
nH, metallicityZsolar, and total energy fromuold, using one loop per quantity.Warning
Do not combine multiple field extractions into a single loop:
! BAD: hurts vectorisation do i = 1, nleaf nH(i) = MAX(uold(ind_leaf(i), 1), smallr) T2(i) = uold(ind_leaf(i), neul) Zsolar(i) = uold(ind_leaf(i), imetal) / nH(i) / 0.02d0 end doUse a separate loop for each quantity so the compiler can vectorise the inner loop efficiently.
2.3. Compute thermal energy — subtracts kinetic energy, radiation energy (if
NENER > 0), and magnetic energy from the total to isolate the thermal part. Converts to \(\Tmu\) in Kelvin viascale_T2.2.4. Self-shielding — if
self_shieldingis enabled, the UV background factor is attenuated exponentially at high densities with a boost factor:\[\mathrm{boost}(i) = \max\!\left( \exp\!\left(-\frac{\nh}{0.01\,\mathrm{cm}^{-3}}\right), 10^{-20} \right).\]This is a simple “poor man’s” model: dense gas that would be self-shielded sees a suppressed UVB. Note that since cooling/heating tables are already computed as a function of \(\Tmu\) and \(n_H\), the boost is in fact applied to density. This is a good approximation, as the dependence of cooling and heating on the radiation intensity is really through a term \(\Gamma/n_H\), so decreasing \(\Gamma\) or increasing \(n_H\) by the same factor has the same effect.
2.5. Polytrope and temperature floor — A minimum thermal energy is in general included. This may be because
barotropic_eosis true, or becausejeans_ncells>0, or becauseT2_star >0. This energy is expressed as a temperature floor (T2min), and needs to be computed and subtracted from the thermal energy before computing cooling. It is then inserted back in step 2.8. below.2.6. Cooling integration — calls
solve_cooling, which returnsdelta_T2, the change in \(\Tmu\) over the hydro timestep.2.7. Delayed cooling — if
delayed_coolingis active, cooling is suppressed in blast-wave regions. When the delay tracer scalar (idelay) exceeds a threshold,delta_T2is clamped to zero (no cooling applied).
- 2.8. Energy update — adds the polytrope floor energy back, as well as other energy terms (kinetic etc.) and
writes the updated total energy to
uold:uold(...) = T2(i) + T2min(i) + ekk(i) + err(i) + emag(i)
The call to solve_cooling (in hydro/cooling_module.f90) is really the heart of
the thermochemistry. Here the temperature-change of every cell in the
vector is evolved, sub-cycling if needed with timestep lengths
\(\dtcool\propto\frac{T}{\Lambda}\). Within each
sub-step:
Obtain \(\CH\) and \(\CHp\) by bilinear interpolation in the pre-computed 2D tables at the current \((\Tmu, \nh)\).
Compute the sub-step size \(\delta t\) from the local cooling time \(\dtcool(\Tmu)\).
Apply the semi-implicit update:
\[\Tmu^{t+\delta t} = \Tmu^{t} + \frac{\CH K \delta t}{1 - \CHp K \delta t}.\]Advance the cell clock: \(t_{\mathrm{cell}} \leftarrow t_{\mathrm{cell}} + \delta t\). When \(t_{\mathrm{cell}} \geq \dthyd\), the cell is finished.
At the end, return \(\Delta\Tmu = \Tmu^{\mathrm{final}} - \Tmu^{\mathrm{init}}\).
Non-equilibrium cooling#
This works quite similarly as the default equilibrium cooling, except, here, the non-equilibrium fraction of ionised hydrogen, and, optionally, HeII, HeIII, and neutral hydrogen (implicitly evolving molecular hydrogen) is evolved and tracked in every cell. These ionisation fractions are stored in passive scalars, usually right after the metallicity scalar. Here, the ionization fractions are evolved along with the temperature in each cooling sub-cycling step in a quasi-implicit fashion (see the RAMSES-RT paper by Rosdahl et al. 2013).
The non-equilibrium cooling module was written specifically for
radiative transfer (see again the RAMSES-RT paper) and is found in
rt/rt_cooling_module.f90. It contains the routine
rt_solve_cooling, called instead of the default solve_cooling
from cooling_fine if the non-equilibrium cooling is activated. In
this case, cooling fine updates not only the pressure variable in
uold, but also the passive scalars corresponding to the ionization
fractions of hydrogen and helium (and, if radiative transfer is also
activated, the momentum of the gas from radiation-gas interactions and
the photon fluxes and densities). The non-equilibrium cooling can only
be activated if RAMSES is compiled with the -RT flag. However, if
the flag is used, non-equilibrium cooling can still be activated without
radiative transfer, simply by compiling with zero radiation groups, and
using cooling=.true. and neq_chem=.true. in the
COOLING_PARAMS namelist.
As for the equilibrium cooling module, cooling and heating rates are tabulated, though in this case only against temperature (in Kelvin), whereas the equilibrium cooling rates can be tabulated against both temperature and gas density. The reason for this is that the cooling rates depend on the ionisation fractions of the gas species, which are direct functions of temperature in equilibrium, but not in non-equilibrium (and tabulating against density is preferrable if possible, since it reduces the computational cost compared to multiplying the cooling rates with densities).
The non-equilibrium heating and cooling rates tables are initialised in
rt_set_table (called during init_time) and updated every coarse
timestep from amr_step. For the cooling rates, only Compton cooling
actually needs to be updated, since the others are not
redshift-dependent (with equilibrium cooling all the cooling rates are
redshift-dependent trough the PIE ionization fractions).
With non-equilibrium cooling, the homogeneous UV background is more flexible than the hardcoded variants in equilibrium cooling. It can be read from files.
ISM-cooling#
Exercise: figure out how it works.