Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cooling Module

Radiative cooling in Quokka is selected at compile time by the EOSBackend member of quokka::EOS_Traits<problem_t>. The backend determines both how gas temperature is computed and whether a cooling source term is applied at all. Selecting the tabulated backend enables the cooling module; the integrator can then be toggled at runtime by cooling.enabled (on by default).

Choosing an EOS backend

BackendCoolingTemperature from
quokka::EOSIdeal<P>noneideal gas law with constant \(\mu\)
quokka::EOSTabulated<P>tabulated (Grackle-derived)resampled table lookup
quokka::EOSMicrophysics<P>Microphysics reaction networkMicrophysics EOS

A problem selects its backend in its EOS_Traits specialization:

template <> struct quokka::EOS_Traits<MyProblem> {
    static constexpr double gamma = 5. / 3.;
    static constexpr double mean_molecular_weight = C::m_u;
    using EOSBackend = quokka::EOSTabulated<MyProblem>;
};

If EOSBackend is omitted, the default is EOSMicrophysics<P> when the target is compiled with CHEMISTRY or PHOTOCHEMISTRY defined, and EOSIdeal<P> otherwise.

EOSIdeal

The gamma-law ideal gas EOS, and the default for most problems. Temperature follows from \(e_{\rm int} = k_B T / [(\gamma - 1) \mu m_u]\) with the constant mean_molecular_weight from EOS_Traits. No cooling source term is applied, and cooling.* parameters have no effect.

EOSTabulated

Applies optically-thin radiative cooling as a Strang-split source term, and computes temperature by interpolating a pre-computed table of \(T(\rho, e_{\rm int})\). This is the backend used by the ISM and galaxy problems (ShockCloud, TallBoxSf, DiskGalaxy, RandomBlast, SN, ParticleSF). It is described in detail below.

EOSMicrophysics

Delegates the EOS to the Microphysics submodule, so that temperature is consistent with the evolving chemical species tracked by the reaction network (chemstate.xn). Cooling and heating are handled by the network itself rather than by a table, and are advanced by the chemistry burner (quokka::chemistry::computeChemistry) rather than by the cooling integrator.

This is the backend for every target compiled with CHEMISTRY or PHOTOCHEMISTRY, which covers both primordial chemistry problems (PrimordialChem, PopIII) and photoionization problems (DTypeFront, DTypeFrontVC, StromgrenSphere, StromgrenSphereRSLA, OneZonePhotoionization) — including hydro-enabled ones. None of these set EOSBackend explicitly; they receive EOSMicrophysics through DefaultEOSBackend by virtue of their compile definition. See the chemistry module and Photoionization for details.

The EOSTabulated cooling model

Each hydrodynamic timestep, the cooling operator integrates the internal energy of every cell at fixed density (isochoric cooling):

The first term is interpolated from the table; the second is a spatially uniform heating rate per hydrogen atom (erg s⁻¹ H⁻¹), where \(n_H = \rho , X_H / m_p\) and \(X_H\) is the Cloudy hydrogen mass fraction stored in the table. \(\Gamma\) is the sum of the SFH-based photoelectric heating rate and the heating_rate_external parser expression; both are zero by default.

The tabulated term \(\dot{E}_{\rm tab}\) bundles three physical processes, all baked into the table when it is generated:

  1. Grackle/Cloudy heating and cooling — primordial and metal heating and cooling rates, plus UV background photo-heating, read from the Grackle data files. Metal rates are tabulated at solar metallicity and scaled linearly by the metallicity chosen at generation time (--zmet).
  2. Photoelectric heating — grain photoelectric heating using the \(\epsilon(G_0, T, n_e, \phi_{\rm PAH})\) prescription of Wolfire et al. (2003), with \(G_0 = 1.7\) and \(\phi_{\rm PAH} = 0.5\), scaled by metallicity. Included only in tables generated without --exclude_pe.
  3. Inverse Compton cooling off CMB photons — \(\propto -(T - T_{\rm CMB}) , n_e\) with \(T_{\rm CMB} = 2.725\) K, following e.g. Hirata (2018). Always included.

The ODE is integrated with an adaptive embedded RK2 (Heun’s method) sub-cycle at a relative tolerance of \(10^{-4}\). If temperature_floor is set, the result is clamped to the corresponding internal energy and the floor also sets the absolute integration tolerance. If a cell exceeds the maximum substep count, the cooling operator reports failure and the hydro update is retried.

Cooling tables

Each table covers a 2D grid of \((\rho, e_{\rm int})\) with fast_log coordinate spacing (a bit-level approximation to log₂) and stores five outputs as raw physical values:

IndexQuantityUnits
0Cooling rate \(\dot{E}_{\rm tab} = \dot{E}/\rho^2\)erg cm³ g⁻² s⁻¹
1TemperatureK
2Sound speedcm s⁻¹
3Pressuredyn cm⁻²
4Entropy \(K = k_B T , n^{-2/3}\)erg cm²

Available table files in extern/cooling/:

FileUV backgroundPhotoelectric heating
CloudyData_UVB=HM2012_resampled.h5HM2012included
CloudyData_UVB=HM2012_resampled_noPE.h5HM2012excluded
CloudyData_UVB=HM2012_shielded_resampled.h5HM2012 (shielded)included
CloudyData_UVB=HM2012_shielded_resampled_noPE.h5HM2012 (shielded)excluded
isrf_1000Go_grains_resampled.h5ISRF 1000 G₀ + grainsincluded

All bundled tables are generated at solar metallicity and redshift zero. To regenerate the four Grackle-based tables, run extern/cooling/resample_grackle_cooling_tables.sh; to regenerate isrf_1000Go_grains_resampled.h5, run extern/cooling/resample_cloudy_cooling_tables.py directly.

Source Grackle data files

The Grackle-based tables are resampled from the Cloudy data files distributed with Grackle, which are downloaded automatically by resample_grackle_cooling_tables.py. These files store heating and cooling rates for primordial and metal species along with UV background photo-heating and photo-ionization rates, valid over \(-10 < \log_{10}(n_H / {\rm cm^{-3}}) < 4\) and \(1 < \log_{10}(T / {\rm K}) < 9\); Grackle extrapolates outside this range. Quokka uses two of them:

  • CloudyData_UVB=HM2012.h5 — rates with the Haardt & Madau (2012) UV background. Collisional ionization equilibrium is assumed above redshift 15.13.
  • CloudyData_UVB=HM2012_shielded.h5 — the same UV background, but recomputed with Jeans-length depth models (capped at 100 pc) so that metal line cooling is correct in self-shielded regions. Using the optically-thin table where self-shielding matters can overestimate the net cooling rate by an order of magnitude at some densities.

Note: the optically-thin HM2012 file is known to have unphysically high heating rates at low densities (grackle_data_files issue #7). Grackle recommends against it for fully tabulated cooling; prefer the shielded table for ISM problems.

Note: Output transform metadata is intentionally absent from the HDF5 files. Output transforms are a property of the interpolation, not of the data; they are declared at the C++ call site in ResampledCooling.cpp. HDF5 files always contain raw physical values. When H5Reader loads a fast_log output, it applies FastMath::inverse_pow2 (Newton iteration) to each value at load time, so the internal buffer stores the interpolation-space representation that FastMath::pow2 inverts during lookup.

HDF5 table format

All cooling tables follow the self-describing tab1 group format read by quokka::DataTable. The group tab1 inside each HDF5 file contains:

  • Dataset data — shape [5, Nx0, Nx1] (C row-major), holding the five output quantities stacked along the first axis.

  • Dataset grids/rho — physical density grid values (g/cm³), informational only.

  • Dataset grids/eint — physical specific internal energy grid values (erg/g), informational only.

  • Attributes on tab1:

    AttributeTypeDescription
    Ndimint32Number of input dimensions (2)
    Noutint32Number of output quantities (5)
    Nxint32[2]Grid sizes [n_rho, n_eint]
    xlofloat64[2]Physical lower bounds [rho_min, eint_min]
    xhifloat64[2]Physical upper bounds [rho_max, eint_max]
    spacingstring[2]Coordinate spacing type ("fast_log" for both)
    input_namesstring[2]["rho", "eint"]
    output_namesstring[5]Names for the five outputs
    input_unitsstring[2]Physical units of inputs
    output_unitsstring[5]Physical units of outputs
    include_peint321 if photoelectric heating is included, else 0
    cloudy_H_mass_fractionfloat64Hydrogen mass fraction assumed in Cloudy

    String attributes must use fixed-length HDF5 strings (numpy dtype='S'); variable-length HDF5 strings are not supported by H5Reader.

Old-format files (pre-tab1) can be converted in-place with scripts/python/convert_cooling_table_hdf5.py.

Runtime parameters

ParameterTypeDefaultDescription
cooling.enabledbool (0/1)1Only takes effect with the EOSTabulated backend. Set to 0 to skip the cooling integrator while still using the table for temperature — useful for testing.
cooling.hdf5_data_filestringrequired with EOSTabulatedPath to the cooling table HDF5 file.
cooling.cooling_table_typestring"resampled"Table type. Only "resampled" is supported.
cooling.read_tables_even_if_disabledbool (0/1)0Read tables at startup even when the problem does not use EOSTabulated (useful for diagnostics).
heating_rate_externalstring""AMReX parser expression for a time-variable external heating rate per H atom (erg s⁻¹ H⁻¹). Variables: time, dt; constants: yr, kyr, Myr, Gyr.
temperature_floorfloat0.0Minimum temperature (K). Clamps the cooling integration and sets its absolute tolerance.

See Runtime parameters for the full parameter table, including the star formation history parameters below.

Star-formation-history-based photoelectric heating

By default, photoelectric heating is a static part of the table, computed for a fixed \(G_0 = 1.7\). For problems where the ISRF should instead track the star formation history of the simulation itself, set use_sfh_based_pe_heating = 1. Quokka then computes a time-variable, spatially uniform heating rate

by convolving the recorded stellar mass increments with a weight function \(w\) tabulated against stellar population age. The bundled table extern/cooling/photoelectric_heating_from_sfh.csv was generated with SLUG and is normalized so that the solar-neighbourhood star formation rate surface density (2.5 × 10⁻³ M⊙ kpc⁻² yr⁻¹) gives \(\Gamma_{\rm PE} = 2 \times 10^{-26}\) erg s⁻¹ H⁻¹.

This path is only active in 3D. Enabling it requires (all enforced by assertions at startup):

  • a cooling table without photoelectric heating (a _noPE file), since the table would otherwise double-count it;
  • sfh_to_pe_heating_table pointing at the weight-function CSV;
  • either star formation history recording (sfh_interval or sfh_time_interval) together with sf_area_kpc2, or a constant const_sfr_Msun_per_year_per_kpc2 in place of a real history.

The TallBoxSf problem is the reference configuration:

cooling.hdf5_data_file = "../extern/cooling/CloudyData_UVB=HM2012_shielded_resampled_noPE.h5"
use_sfh_based_pe_heating = true
sfh_to_pe_heating_table = "../extern/cooling/photoelectric_heating_from_sfh.csv"
sfh_time_interval = 3.155760000e+12  # 1e5 yr
sf_area_kpc2 = 1.0  # kpc^2
heating_rate_external = "max(min(1.0, 2.0 - time / (32 * Myr)), 0.0024) * 2e-26"

Using cooling in a problem

Tabulated cooling is the only cooling module currently supported: a problem gets radiative cooling by selecting the EOSTabulated backend, and cooling.cooling_table_type accepts only "resampled". (EOSMicrophysics evolves its own thermochemistry through the reaction network, but that is the chemistry burner rather than this cooling operator.) The rest of this section covers setting up and using the tabulated backend.

1. Select the tabulated backend

template <> struct quokka::EOS_Traits<MyProblem> {
    static constexpr double gamma = 5. / 3.;
    static constexpr double mean_molecular_weight = C::m_u;
    using EOSBackend = quokka::EOSTabulated<MyProblem>;
};

2. Point the input file at a table

cooling.hdf5_data_file = "../extern/cooling/CloudyData_UVB=HM2012_resampled.h5"

Quokka reads the table at startup and calls quokka::ResampledCooling::computeCooling() as a Strang-split operator each timestep. Cooling requires a non-isothermal EOS; it aborts if gamma = 1. It also cannot be combined with photoionization (photochemistry.enabled = 1), which models the same hydrogen thermochemistry — see Photoionization.

3. Choose how photoelectric heating is treated

The table file and use_sfh_based_pe_heating together select one of three mutually exclusive treatments of grain photoelectric heating:

TreatmentTable fileuse_sfh_based_pe_heating
No photoelectric heating_noPE0 (default)
Constant PE heating, baked into the tablewithout _noPE0 (default)
Time-variable PE heating from the star formation rate_noPE1
  • Ignore photoelectric heating — use a _noPE table. Appropriate where the ISRF is irrelevant or supplied by other means.
  • Constant PE heating — use a table without _noPE. The Wolfire et al. (2003) rate for a fixed \(G_0 = 1.7\) is baked in at table-generation time, so it costs nothing at runtime but cannot respond to the simulation.
  • Star-formation-rate-based PE heating — use a _noPE table and set use_sfh_based_pe_heating = 1, so the ISRF tracks the stars the simulation forms. See the section below for the required parameters.

The third option requires a _noPE table because the SFH-based rate replaces the table’s static PE term rather than supplementing it; pairing it with a PE-inclusive table would double-count the heating. Quokka enforces this at startup by checking the table’s include_pe attribute (the filename is only a naming convention) and aborting on a mismatch.

An external heating_rate_external expression is independent of this choice and is simply added to whichever rate applies.

4. Access thermodynamic quantities in callbacks

Prefer quokka::EOS<problem_t>, which forwards to the selected backend and therefore works for any problem:

const Real T = quokka::EOS<MyProblem>::ComputeTgasFromEint(rho, Eint);
const Real Eint = quokka::EOS<MyProblem>::ComputeEintFromTgas(rho, T);   // root-finds through the table
const Real K = quokka::EOS<MyProblem>::ComputeEntropyFromRhoEint(rho, Eint);  // EOSTabulated only

For table-specific quantities, the GPU-safe helpers in src/cooling/ResampledCooling.hpp interpolate directly. They take a resampledGpuConstTables handle, obtained inside QuokkaSimulation member functions from resampledTables_.const_tables() (or const_tables_host() for host-side use), and captured by value into the lambda:

#include "cooling/ResampledCooling.hpp"

auto tables = resampledTables_.const_tables();
amrex::ParallelFor(box, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept {
    // given rho (g/cm³) and Eint (erg/cm³):
    const Real P = quokka::ResampledCooling::ComputePressureFromRhoEint(rho, Eint, tables);
    const Real cs = quokka::ResampledCooling::ComputeSoundSpeedFromRhoEint(rho, Eint, tables);
    const Real Edot = quokka::ResampledCooling::resampled_cooling_function(rho, Eint, tables);
});

5. Cooling length estimate

const Real l_cool = quokka::ResampledCooling::ComputeCoolingLength(rho, Eint, tables);

This returns \(c_s , t_{\rm cool}\) at the given state and is useful for AMR refinement criteria; ShockCloud uses it to tag cells whose cooling length is unresolved.

Python utilities

The scripts in extern/cooling/ support table generation and analysis:

ScriptPurpose
resample_grackle_cooling_tables.pyGenerate Grackle-based cooling tables from the Cloudy HDF5 data.
resample_grackle_cooling_tables.shShell wrapper that regenerates all four bundled Grackle tables.
resample_cloudy_cooling_tables.pyGenerate cooling tables directly from Cloudy output (used for ISRF table).
integrate_cooling_zone.pyNumerically integrate a single cooling zone and compare against Grackle directly.
test_cooling_onezone.pyOne-zone cooling test using integrate_cooling_zone.py.
grackle_tables.pyLow-level reader for the raw Grackle Cloudy HDF5 data, and the reference implementation of the cooling, photoelectric, and Compton terms.

Install Python dependencies with:

pip install -r extern/cooling/requirements.txt

Test problem

The ResampledCoolingTest problem (src/problems/ResampledCoolingTest/) verifies the cooling integrator by comparing the simulated temperature evolution of a single cell against a Grackle reference solution. The test passes when the L1 error between the two trajectories is below 2%.

To build and run:

quokka buildrun -d 1d ResampledCoolingTest