diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/README b/examples/RadiativeTransferTests/CosmoCoolingTest/README new file mode 100644 index 0000000000000000000000000000000000000000..eb50535a856ff2a0a09f1440272ee9f22601d7e5 --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoCoolingTest/README @@ -0,0 +1,7 @@ +Check on a simple case whether the cooling works correctly without any +radiation being present: Use a uniform 3D box of hot gas and let it cool down. + +The reference solution assumes case A recombination. + +To run with GEAR-RT, compile swift with: + --with-rt=GEAR_3 --with-rt-riemann-solver=GLF --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/getReference.sh b/examples/RadiativeTransferTests/CosmoCoolingTest/getReference.sh new file mode 100755 index 0000000000000000000000000000000000000000..a6c154ac1594e94374f468f3059c6e26fb58db3d --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoCoolingTest/getReference.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +wget http://virgodb.cosma.dur.ac.uk/swift-webstorage/ReferenceSolutions/CosmoRTCoolingTestReference.txt diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/makeIC.py b/examples/RadiativeTransferTests/CosmoCoolingTest/makeIC.py new file mode 100755 index 0000000000000000000000000000000000000000..b727376480e8267bd36dd15e6c0eb044a921814d --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoCoolingTest/makeIC.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +############################################################################### +# This file is part of SWIFT. +# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com) +# 2024 Stan Verhoeve (s06verhoeve@gmail.com) +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +# ----------------------------------------------------------- +# Use 10 particles in each dimension to generate a uniform +# box with high temperatures. +# ----------------------------------------------------------- + +import h5py +import numpy as np +import unyt +from swiftsimio import Writer +from swiftsimio.units import cosmo_units +import yaml + +with open(r"rt_cooling_test.yml") as paramfile: + params = yaml.load(paramfile, Loader=yaml.FullLoader) + a_begin = params["Cosmology"]["a_begin"] + a_begin = float(a_begin) + + +# number of particles in each dimension +n_p = 10 +nparts = n_p ** 3 +# filename of ICs to be generated +outputfilename = "cooling_test.hdf5" +# adiabatic index +gamma = 5.0 / 3.0 +# total hydrogen mass fraction +XH = 0.76 +# total helium mass fraction +XHe = 0.24 +# boxsize +boxsize = 1 * unyt.kpc +# initial gas temperature +initial_temperature = 1e6 * unyt.K + +# Initial particle density and mass +gas_density_phys_cgs = 1.6756058890024518e-25 * unyt.g / unyt.cm ** 3 + +# Include a^3 to convert physical density to comoving +pmass = (gas_density_phys_cgs) * (boxsize ** 3 / nparts) * a_begin ** 3 +pmass = pmass.to("Msun") +# ----------------------------------------------- + + +def internal_energy(T, mu): + """ + Compute the internal energy of the gas for a given + temperature and mean molecular weight + """ + # Using u = 1 / (gamma - 1) * p / rho + # and p = N/V * kT = rho / (mu * m_u) * kT + + u = unyt.boltzmann_constant * T / (gamma - 1) / (mu * unyt.atomic_mass_unit) + return u + + +def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp): + """ + Determines the mean molecular weight for given + mass fractions of + hydrogen: XH0 + H+: XHp + He: XHe0 + He+: XHep + He++: XHepp + + returns: + mu: mean molecular weight [in atomic mass units] + NOTE: to get the actual mean mass, you still need + to multiply it by m_u, as is tradition in the formulae + """ + + # 1/mu = sum_j X_j / A_j * (1 + E_j) + # A_H = 1, E_H = 0 + # A_Hp = 1, E_Hp = 1 + # A_He = 4, E_He = 0 + # A_Hep = 4, E_Hep = 1 + # A_Hepp = 4, E_Hepp = 2 + one_over_mu = XH0 + 2 * XHp + 0.25 * XHe0 + 0.5 * XHep + 0.75 * XHepp + + return 1.0 / one_over_mu + + +# assume everything is ionized initially +mu = mean_molecular_weight(0.0, XH, 0.0, 0.0, XHe) +u_part = internal_energy(initial_temperature, mu) +pmass = pmass.to("Msun") + + +xp = unyt.unyt_array(np.zeros((nparts, 3), dtype=np.float32), boxsize.units) +dx = boxsize / n_p +ind = 0 +for i in range(n_p): + x = (i + 0.5) * dx + for j in range(n_p): + y = (j + 0.5) * dx + for k in range(n_p): + z = (k + 0.5) * dx + + xp[ind] = (x, y, z) + ind += 1 + +w = Writer(cosmo_units, boxsize, dimension=3) + +w.gas.coordinates = xp +w.gas.velocities = np.zeros(xp.shape, dtype=np.float32) * (unyt.km / unyt.s) +w.gas.masses = np.ones(nparts, dtype=np.float32) * pmass +w.gas.internal_energy = np.ones(nparts, dtype=np.float32) * u_part + +# Generate initial guess for smoothing lengths based on MIPS +w.gas.generate_smoothing_lengths(boxsize=boxsize, dimension=3) + +# If IDs are not present, this automatically generates +w.write(outputfilename) + + +# Now open file back up again and add RT data. +F = h5py.File(outputfilename, "r+") +header = F["Header"] +parts = F["/PartType0"] + +# Create initial ionization species mass fractions. +# Assume everything is ionized initially +# NOTE: grackle doesn't really like exact zeroes, so +# use something very small instead. +HIdata = np.ones(nparts, dtype=np.float32) * 1e-12 +HIIdata = np.ones(nparts, dtype=np.float32) * XH +HeIdata = np.ones(nparts, dtype=np.float32) * 1e-12 +HeIIdata = np.ones(nparts, dtype=np.float32) * 1e-12 +HeIIIdata = np.ones(nparts, dtype=np.float32) * XHe + +parts.create_dataset("MassFractionHI", data=HIdata) +parts.create_dataset("MassFractionHII", data=HIIdata) +parts.create_dataset("MassFractionHeI", data=HeIdata) +parts.create_dataset("MassFractionHeII", data=HeIIdata) +parts.create_dataset("MassFractionHeIII", data=HeIIIdata) + +# close up, and we're done! +F.close() diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/plotSolution.py b/examples/RadiativeTransferTests/CosmoCoolingTest/plotSolution.py new file mode 100755 index 0000000000000000000000000000000000000000..fb4e103ef12d1c68ebff3c6a23874ba086d565a1 --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoCoolingTest/plotSolution.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 +############################################################################### +# This file is part of SWIFT. +# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com) +# 2024 Stan Verhoeve (s06verhoeve@gmail.com) +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +# ------------------------------------------- +# Plot the gas temperature, mean molecular +# weight, and mass fractions +# ------------------------------------------- + +import copy +import os + +import numpy as np +import swiftsimio +import unyt +from matplotlib import pyplot as plt + +# arguments for plots of results +plotkwargs = {"alpha": 0.5} +# arguments for plot of references +referenceplotkwargs = {"color": "grey", "lw": 4, "alpha": 0.6} +# arguments for legends +legendprops = {"size": 8} +# snapshot basenames +snapshot_base = "output" +# Reference file name +reference_file = "CosmoRTCoolingTestReference.txt" +# Plot time on x-axis +plot_time = False + + +# ----------------------------------------------------------------------- +energy_units = unyt.Msun * unyt.kpc ** 2 / unyt.kyr ** 2 +mass_units = unyt.Msun +length_units = unyt.kpc + + +def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp): + """ + Determines the mean molecular weight for given + mass fractions of + hydrogen: XH0 + H+: XHp + He: XHe0 + He+: XHep + He++: XHepp + + returns: + mu: mean molecular weight [in atomic mass units] + NOTE: to get the actual mean mass, you still need + to multiply it by m_u, as is tradition in the formulae + """ + + # 1/mu = sum_j X_j / A_j * (1 + E_j) + # A_H = 1, E_H = 0 + # A_Hp = 1, E_Hp = 1 + # A_He = 4, E_He = 0 + # A_Hep = 4, E_Hep = 1 + # A_Hepp = 4, E_Hepp = 2 + one_over_mu = XH0 + 2 * XHp + 0.25 * XHe0 + 0.5 * XHep + 0.75 * XHepp + + return 1.0 / one_over_mu + + +def gas_temperature(u, mu, gamma): + """ + Compute the gas temperature given the specific internal + energy u and the mean molecular weight mu + """ + + # Using u = 1 / (gamma - 1) * p / rho + # and p = N/V * kT = rho / (mu * m_u) * kT + + T = u * (gamma - 1) * mu * unyt.atomic_mass_unit / unyt.boltzmann_constant + + return T + + +def get_snapshot_list(snapshot_basename="output"): + """ + Find the snapshot(s) that are to be plotted + and return their names as list + """ + + snaplist = [] + + dirlist = os.listdir() + for f in dirlist: + if f.startswith(snapshot_basename) and f.endswith("hdf5"): + snaplist.append(f) + + if len(snaplist) == 0: + raise FileNotFoundError( + "Didn't find any snapshots with basename '" + snapshot_basename + "'" + ) + + snaplist = sorted(snaplist) + + return snaplist + + +def get_ion_mass_fractions(swiftsimio_loaded_data): + """ + Returns the ion mass fractions according to + the used scheme. + + swiftsimio_loaded_data: the swiftsimio.load() object + """ + + data = swiftsimio_loaded_data + meta = data.metadata + gas = data.gas + with_rt = True + scheme = None + try: + scheme = str(meta.subgrid_scheme["RT Scheme"].decode("utf-8")) + except KeyError: + # allow to read in solutions with only cooling, without RT + with_rt = False + + if with_rt: + if scheme.startswith("GEAR M1closure"): + imf = data.gas.ion_mass_fractions + elif scheme.startswith("SPH M1closure"): + # atomic mass + mamu = { + "e": 0.0, + "HI": 1.0, + "HII": 1.0, + "HeI": 4.0, + "HeII": 4.0, + "HeIII": 4.0, + } + mass_function_hydrogen = data.gas.rt_element_mass_fractions.hydrogen + imf = copy.deepcopy(data.gas.rt_species_abundances) + named_columns = data.gas.rt_species_abundances.named_columns + for column in named_columns: + # abundance is in n_X/n_H unit. We convert it to mass fraction by multipling mass fraction of H + mass_function = ( + getattr(data.gas.rt_species_abundances, column) + * mass_function_hydrogen + * mamu[column] + ) + setattr(imf, column, mass_function) + else: + raise ValueError("Unknown scheme", scheme) + else: + # try to find solutions for cooling only runs + imf = { + "HI": gas.hi[:], + "HII": gas.hii[:], + "HeI": gas.he_i[:], + "HeII": gas.he_ii[:], + "HeIII": gas.he_iii[:], + } + + return imf + + +def get_snapshot_data(snaplist): + """ + Extract the relevant data from the list of snapshots. + + Returns: + numpy arrays of: + time + temperatures + mean molecular weights + mass fractions + """ + + nsnaps = len(snaplist) + firstdata = swiftsimio.load(snaplist[0]) + with_rt = True + try: + ngroups = int(firstdata.metadata.subgrid_scheme["PhotonGroupNumber"]) + except KeyError: + # allow to read in solutions with only cooling, without RT + with_rt = False + + # Read internal units + unit_system = firstdata.metadata.units + mass_units = unit_system.mass + length_units = unit_system.length + time_units = unit_system.time + + # Units derived from base + velocity_units = length_units / time_units + energy_units = mass_units * velocity_units ** 2 + density_units = mass_units / length_units ** 3 + + scale_factors = np.zeros(nsnaps) + times = np.zeros(nsnaps) * unyt.Myr + temperatures = np.zeros(nsnaps) * unyt.K + mean_molecular_weights = np.zeros(nsnaps) + mass_fractions = np.zeros((nsnaps, 5)) + internal_energies = np.zeros(nsnaps) * energy_units + specific_internal_energies = np.zeros(nsnaps) * energy_units / mass_units + densities = np.zeros(nsnaps) * density_units + + if with_rt: + photon_energies = np.zeros((ngroups, nsnaps)) * energy_units + else: + photon_energies = None + + for i, snap in enumerate(snaplist): + + data = swiftsimio.load(snap) + gamma = data.gas.metadata.gas_gamma[0] + time = data.metadata.time.copy() + scale_factor = data.metadata.scale_factor.copy() + + gas = data.gas + u = gas.internal_energies[:].to(energy_units / mass_units) + u.convert_to_physical() + + rho = gas.densities + rho.convert_to_physical() + rho.to(density_units) + rho.convert_to_physical() + + masses = gas.masses[:].to(mass_units) + masses.convert_to_physical() + + imf = get_ion_mass_fractions(data) + mu = mean_molecular_weight(imf.HI, imf.HII, imf.HeI, imf.HeII, imf.HeIII) + mu.convert_to_physical() + + T = gas_temperature(u, mu, gamma).to("K") + um = u.to(energy_units / mass_units) * masses + um.to(energy_units) + + scale_factors[i] = scale_factor + times[i] = time.to("Myr") + temperatures[i] = np.mean(T) + mean_molecular_weights[i] = np.mean(mu) + internal_energies[i] = np.mean(um) + specific_internal_energies[i] = u.to(energy_units / mass_units).sum() / len(u) + densities[i] = rho.sum() / len(rho) + + mass_fractions[i, 0] = np.mean(imf.HI) + mass_fractions[i, 1] = np.mean(imf.HII) + mass_fractions[i, 2] = np.mean(imf.HeI) + mass_fractions[i, 3] = np.mean(imf.HeII) + mass_fractions[i, 4] = np.mean(imf.HeIII) + + if with_rt: + for g in range(ngroups): + en = getattr(data.gas.photon_energies, "group" + str(g + 1)) + en = en[:].to_physical().to(energy_units) + photon_energies[g, i] = en.sum() / en.shape[0] + + return ( + scale_factors, + times, + temperatures, + mean_molecular_weights, + mass_fractions, + internal_energies, + photon_energies, + specific_internal_energies, + densities, + ) + + +def get_reference_data(reference_file="CosmoRTCoolingTestReference.txt"): + # Grab lines containing units + with open(reference_file) as f: + # Skip first line + f.readline() + mass_units_line = f.readline() + length_units_line = f.readline() + velocity_units_line = f.readline() + + # Extract numbers from lines + mass_units = float(mass_units_line[18:-4]) * unyt.g + length_units = float(length_units_line[20:-5]) * unyt.cm + velocity_units = float(velocity_units_line[22:-7]) * unyt.cm / unyt.s + + # Derived units + energy_units = mass_units * velocity_units ** 2 + density_units = mass_units / length_units ** 3 + + # Read in data + refdata = np.loadtxt(reference_file, dtype=np.float64) + + a_ref = refdata[:, 1] + t_ref = refdata[:, 3] * unyt.yr + dt_ref = refdata[:, 4] * unyt.yr + T_ref = refdata[:, 5] * unyt.K + mu_ref = refdata[:, 6] + rho_ref = refdata[:, 7] * density_units + rhoHI_ref = refdata[:, 8] * density_units + rhoHII_ref = refdata[:, 9] * density_units + rhoHeI_ref = refdata[:, 10] * density_units + rhoHeII_ref = refdata[:, 11] * density_units + rhoHeIII_ref = refdata[:, 12] * density_units + rhoe_ref = refdata[:, 13] * density_units + u_ref = refdata[:, -1] * energy_units / mass_units + mass_fraction_ref = np.empty((t_ref.shape[0], 5)) + mass_fraction_ref[:, 0] = rhoHI_ref / rho_ref + mass_fraction_ref[:, 1] = rhoHII_ref / rho_ref + mass_fraction_ref[:, 2] = rhoHeI_ref / rho_ref + mass_fraction_ref[:, 3] = rhoHeII_ref / rho_ref + mass_fraction_ref[:, 4] = rhoHeIII_ref / rho_ref + + return ( + a_ref, + t_ref, + T_ref, + mu_ref, + mass_fraction_ref, + dt_ref, + rho_ref, + rhoHI_ref, + rhoHII_ref, + rhoHeI_ref, + rhoHeII_ref, + rhoHeIII_ref, + rhoe_ref, + u_ref, + ) + + +if __name__ == "__main__": + + # Get list of shapshots + snaplist = get_snapshot_list(snapshot_base) + + # Read snapshot data + a, t, T, mu, mass_fraction, u, photon_energies, us, rho = get_snapshot_data( + snaplist + ) + + with_rt = photon_energies is not None + if with_rt: + ngroups = photon_energies.shape[0] + + # Read reference solution data + a_ref, t_ref, T_ref, mu_ref, mass_fraction_ref, *__ = get_reference_data( + reference_file + ) + + # Convert t_ref to Myr + t_ref.convert_to_units("Myr") + + # Translate snapshot times to start at t=0 + t -= t[0] + + # Grab x-coordinate + if plot_time: + xcoords = t + xcoords_ref = t_ref + xlabel = "Time [Myr]" + xscale = "log" + xlims = (0.1, max(t)) + else: + xcoords = a + xcoords_ref = a_ref + xlabel = "Scale factor [1]" + xscale = "linear" + xlims = (min(a), max(a)) + + # ------------------ + # Plot figures + # ------------------ + + fig = plt.figure(figsize=(8, 8), dpi=300) + ax1 = fig.add_subplot(2, 2, 1) + ax2 = fig.add_subplot(2, 2, 2) + ax3 = fig.add_subplot(2, 2, 3) + ax4 = fig.add_subplot(2, 2, 4) + + ax1.set_xscale(xscale) + ax2.set_xscale(xscale) + ax3.set_xscale(xscale) + ax4.set_xscale(xscale) + + ax1.set_xlim(*xlims) + ax2.set_xlim(*xlims) + ax3.set_xlim(*xlims) + ax4.set_xlim(*xlims) + + ax1.semilogy(xcoords_ref, T_ref, label="reference", **referenceplotkwargs) + ax1.semilogy(xcoords, T, label="obtained results") + + ax1.set_yscale("log") + ax1.set_xlabel(xlabel) + ax1.set_ylabel("gas temperature [K]") + ax1.legend(prop=legendprops) + ax1.grid() + + ax2.plot(xcoords_ref, mu_ref, label="reference", **referenceplotkwargs) + ax2.plot(xcoords, mu, label="obtained results") + + ax2.set_xlabel(xlabel) + ax2.set_ylabel("mean molecular weight") + ax2.legend(prop=legendprops) + ax2.grid() + + total_mass_fraction = np.sum(mass_fraction, axis=1) + ax3.plot(xcoords, total_mass_fraction, "k", label="total", ls="-") + + ax3.plot( + xcoords_ref[1:], + mass_fraction_ref[1:, 0], + label="reference", + **referenceplotkwargs, + zorder=0, + ) + ax3.plot(xcoords_ref[1:], mass_fraction_ref[1:, 1], **referenceplotkwargs, zorder=0) + ax3.plot(xcoords_ref[1:], mass_fraction_ref[1:, 2], **referenceplotkwargs, zorder=0) + ax3.plot(xcoords_ref[1:], mass_fraction_ref[1:, 3], **referenceplotkwargs, zorder=0) + ax3.plot(xcoords_ref[1:], mass_fraction_ref[1:, 4], **referenceplotkwargs, zorder=0) + + ax3.plot(xcoords, mass_fraction[:, 0], label="HI", ls=":", **plotkwargs, zorder=1) + ax3.plot(xcoords, mass_fraction[:, 1], label="HII", ls="-.", **plotkwargs, zorder=1) + ax3.plot(xcoords, mass_fraction[:, 2], label="HeI", ls=":", **plotkwargs, zorder=1) + ax3.plot( + xcoords, mass_fraction[:, 3], label="HeII", ls="-.", **plotkwargs, zorder=1 + ) + ax3.plot( + xcoords, mass_fraction[:, 4], label="HeIII", ls="--", **plotkwargs, zorder=1 + ) + ax3.legend(loc="upper right", prop=legendprops) + ax3.set_xlabel(xlabel) + ax3.set_ylabel("gas mass fractions [1]") + ax3.grid() + + if with_rt: + u.convert_to_units(energy_units) + photon_energies.convert_to_units(energy_units) + tot_energy = u + np.sum(photon_energies, axis=0) + ax4.plot( + xcoords, + tot_energy, + label=f"total energy budget", + color="k", + ls="--", + **plotkwargs, + ) + for g in range(ngroups): + ax4.plot( + xcoords, + photon_energies[g, :], + label=f"photon energies group {g + 1}", + **plotkwargs, + ) + ax4.plot(xcoords, u, label="gas internal energy", **plotkwargs) + ax4.set_xlabel(xlabel) + ax4.set_ylabel( + r"energy budget [$" + u.units.latex_representation() + "$]", usetex=True + ) + ax4.legend(prop=legendprops) + ax4.grid() + + plt.tight_layout() + # plt.show() + plt.savefig("cooling_test.png") diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/rt_cooling_test.yml b/examples/RadiativeTransferTests/CosmoCoolingTest/rt_cooling_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..97c3724cb5c51a21d36efee861ce1367ceed400f --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoCoolingTest/rt_cooling_test.yml @@ -0,0 +1,80 @@ +MetaData: + run_name: RT Cooling Test + +# Define the system of units to use internally. +InternalUnitSystem: + UnitMass_in_cgs: 1.98848e43 # 10^10 M_sun in grams + UnitLength_in_cgs: 3.08567758e21 # 1 kpc in cm + UnitVelocity_in_cgs: 1e5 # 1 km/s in cm/s + UnitCurrent_in_cgs: 1 # Amperes + UnitTemp_in_cgs: 1 # Kelvin + + +# Parameters governing the time integration +TimeIntegration: + max_nr_rt_subcycles: 1 + time_begin: 0. # The starting time of the simulation (in internal units). + time_end: 0.100 # The end time of the simulation (in internal units). + dt_min: 1.e-8 # The minimal time-step size of the simulation (in internal units). + dt_max: 4.882814e-03 # The maximal time-step size of the simulation (in internal units). + + +# Parameters governing the snapshots +Snapshots: + basename: output # Common part of the name of output files + output_list_on: 0 + output_list: snaplist_cooling + scale_factor_first: 0.047601 # Time of the first output (in internal units) + delta_time: 1.006 + +# Parameters governing the conserved quantities statistics +Statistics: + scale_factor_first: 0.047601 + delta_time: 1.006 # Time between statistics output + +# Parameters for the hydrodynamics scheme +SPH: + resolution_eta: 1.2348 # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel). + CFL_condition: 0.6 # Courant-Friedrich-Levy condition for time integration. + minimal_temperature: 0. # Kelvin + +# Parameters related to the initial conditions +InitialConditions: + file_name: ./cooling_test.hdf5 # The file to read + periodic: 1 # periodic ICs + +GEARRT: + f_reduce_c: 1.e-9 # This test is without actual radiation, so we don't care about this + CFL_condition: 0.9 # CFL condition for RT, independent of hydro + photon_groups_Hz: [3.288e15, 5.945e15, 13.157e15] # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N + stellar_luminosity_model: const # Which model to use to determine the stellar luminosities. + const_stellar_luminosities_LSol: [0., 0., 0.] # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity. + hydrogen_mass_fraction: 0.76 # total hydrogen mass fraction + stellar_spectrum_type: 0 # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum. + stellar_spectrum_const_max_frequency_Hz: 1.e17 # (Conditional) if stellar_spectrum_type=0, use this maximal frequency for the constant spectrum. + case_B_recombination: 0 # reference solution assumes case A recombination + + +GrackleCooling: + cloudy_table: CloudyData_UVB=HM2012.h5 # Name of the Cloudy Table (available on the grackle bitbucket repository) + with_UV_background: 0 # Enable or not the UV background + redshift: 0 # Redshift to use (-1 means time based redshift) + with_metal_cooling: 0 # Enable or not the metal cooling + provide_volumetric_heating_rates: 0 # (optional) User provide volumetric heating rates + provide_specific_heating_rates: 0 # (optional) User provide specific heating rates + max_steps: 10000 # (optional) Max number of step when computing the initial composition + convergence_limit: 1 # (optional) Convergence threshold (relative) for initial composition + self_shielding_method: 0 # (optional) Grackle (1->3 for Grackle's ones, 0 for none and -1 for GEAR) + primordial_chemistry: 1 + thermal_time_myr: 5 + +Scheduler: + tasks_per_cell: 128 + +Cosmology: # Planck13 (EAGLE flavour) + a_begin: 0.0476 # z~20 + a_end: 0.2 # z~4 + h: 0.6777 + Omega_cdm: 0.2587481 + Omega_lambda: 0.693 + Omega_b: 0.0482519 diff --git a/examples/RadiativeTransferTests/CosmoCoolingTest/run.sh b/examples/RadiativeTransferTests/CosmoCoolingTest/run.sh new file mode 100755 index 0000000000000000000000000000000000000000..890a4b745e973c212ac5b7ddb1b13d932607c2f4 --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoCoolingTest/run.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# make run.sh fail if a subcommand fails +set -e +set -o pipefail + +if [ ! -f ./cooling_test.hdf5 ]; then + echo "creating ICs" + python3 makeIC.py +fi + +# Run SWIFT with RT +../../../swift \ + --hydro \ + --cosmology \ + --threads=4 \ + --verbose=0 \ + --radiation \ + --external-gravity \ + --stars \ + --feedback \ +./rt_cooling_test.yml 2>&1 | tee output.log + +# Wanna run with cooling, but no RT? This should do the trick +# ../../../swift \ +# --hydro \ +# --threads=4 \ +# --verbose=0 \ +# --cooling \ +# ./rt_cooling_test.yml 2>&1 | tee output.log + +if [ ! -f "CosmoRTCoolingTestReference.txt" ]; then + ./getReference.sh +fi +python3 plotSolution.py diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/README b/examples/RadiativeTransferTests/CosmoHeatingTest/README new file mode 100644 index 0000000000000000000000000000000000000000..18ba3a98b493955657afb9b6cdc309af72fbb4cd --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoHeatingTest/README @@ -0,0 +1,6 @@ +Runs a uniform box with radiation initially present. No further radiation +sources are present, and the gas should heat up and ionize, while the +radiation fields should decrease at the same rate. + +To run with GEAR-RT, compile swift with: + --with-rt=GEAR_3 --with-rt-riemann-solver=GLF --with-hydro=gizmo-mfv --with-riemann-solver=hllc --with-stars=GEAR --with-feedback=none --with-grackle=$GRACKLE_ROOT diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/makeIC.py b/examples/RadiativeTransferTests/CosmoHeatingTest/makeIC.py new file mode 100755 index 0000000000000000000000000000000000000000..813c0d8a4b9f66e3274a1f574212e0adbd3bd068 --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoHeatingTest/makeIC.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +############################################################################### +# This file is part of SWIFT. +# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com) +# 2024 Stan Verhoeve (s06verhoeve@gmail.com) +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + + +# ----------------------------------------------------------- +# Use 10 particles in each dimension to generate a uniform box +# with a fixed amount of radiation. +# ----------------------------------------------------------- + +from swiftsimio import Writer +from swiftsimio.units import cosmo_units + +import unyt +import numpy as np +import h5py +import yaml + +# Grab scale factor at start of run +with open(r"rt_heating_test.yml") as paramfile: + params = yaml.load(paramfile, Loader=yaml.FullLoader) + a_begin = params["Cosmology"]["a_begin"] + a_begin = float(a_begin) + +# Reduced speed of light +f_reduce_speed_of_light = 1.0e-6 + +# number of particles in each dimension +n_p = 10 +nparts = n_p ** 3 + +# filename of ICs to be generated +outputfilename = "heating_test.hdf5" +# adiabatic index +gamma = 5.0 / 3.0 +# total hydrogen mass fraction +XH = 0.76 +# total helium mass fraction +XHe = 0.24 +# boxsize +boxsize = 1 * unyt.kpc +# initial gas temperature +initial_temperature = 1e3 * unyt.K +# particle mass +pmass = (unyt.atomic_mass_unit / unyt.cm ** 3) * (boxsize ** 3 / nparts) * a_begin ** 3 +pmass = pmass.to("Msun") + +# ----------------------------------------------- + + +def internal_energy(T, mu): + """ + Compute the internal energy of the gas for a given + temperature and mean molecular weight + """ + # Using u = 1 / (gamma - 1) * p / rho + # and p = N/V * kT = rho / (mu * m_u) * kT + + u = unyt.boltzmann_constant * T / (gamma - 1) / (mu * unyt.atomic_mass_unit) + return u + + +def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp): + """ + Determines the mean molecular weight for given + mass fractions of + hydrogen: XH0 + H+: XHp + He: XHe0 + He+: XHep + He++: XHepp + + returns: + mu: mean molecular weight [in atomic mass units] + NOTE: to get the actual mean mass, you still need + to multiply it by m_u, as is tradition in the formulae + """ + + # 1/mu = sum_j X_j / A_j * (1 + E_j) + # A_H = 1, E_H = 0 + # A_Hp = 1, E_Hp = 1 + # A_He = 4, E_He = 0 + # A_Hep = 4, E_Hep = 1 + # A_Hepp = 4, E_Hepp = 2 + one_over_mu = XH0 + 2 * XHp + 0.25 * XHe0 + 0.5 * XHep + 0.75 * XHepp + + return 1.0 / one_over_mu + + +# assume everything is neutral initially +mu = mean_molecular_weight(XH, 0, XHe, 0.0, 0.0) +u_part = internal_energy(initial_temperature, mu) +pmass = pmass.to("Msun") + + +xp = unyt.unyt_array(np.zeros((nparts, 3), dtype=np.float32), boxsize.units) +dx = boxsize / n_p +ind = 0 +for i in range(n_p): + x = (i + 0.5) * dx + for j in range(n_p): + y = (j + 0.5) * dx + for k in range(n_p): + z = (k + 0.5) * dx + + xp[ind] = (x, y, z) + ind += 1 + +w = Writer(cosmo_units, boxsize, dimension=3) + + +w.gas.coordinates = xp +w.gas.velocities = np.zeros(xp.shape, dtype=np.float32) * (unyt.km / unyt.s) +w.gas.masses = np.ones(nparts, dtype=np.float32) * pmass +w.gas.internal_energy = np.ones(nparts, dtype=np.float32) * u_part + +# Generate initial guess for smoothing lengths based on MIPS +w.gas.generate_smoothing_lengths(boxsize=boxsize, dimension=3) + +# If IDs are not present, this automatically generates +w.write(outputfilename) + +# Now open file back up again and add RT data. + +F = h5py.File(outputfilename, "r+") +header = F["Header"] +nparts = header.attrs["NumPart_ThisFile"][0] +parts = F["/PartType0"] + +# Create initial ionization species mass fractions. +# Assume everything neutral initially +# NOTE: grackle doesn't really like exact zeroes, so +# use something very small instead. +HIdata = np.ones(nparts, dtype=np.float32) * XH +HIIdata = np.ones(nparts, dtype=np.float32) * 1e-12 +HeIdata = np.ones(nparts, dtype=np.float32) * XHe +HeIIdata = np.ones(nparts, dtype=np.float32) * 1e-12 +HeIIIdata = np.ones(nparts, dtype=np.float32) * 1e-12 + +parts.create_dataset("MassFractionHI", data=HIdata) +parts.create_dataset("MassFractionHII", data=HIIdata) +parts.create_dataset("MassFractionHeI", data=HeIdata) +parts.create_dataset("MassFractionHeII", data=HeIIdata) +parts.create_dataset("MassFractionHeIII", data=HeIIIdata) + + +# Add photon groups +nPhotonGroups = 3 + +# with this IC, the radiative cooling is negligible. +# photon_energy = u_part * pmass * 5.0 +# photon_energy = np.arange(1, nPhotonGroups+1) * photon_energy + +# Fluxes from the Iliev Test0 part3 +fluxes_iliev = np.array([1.350e1, 2.779e1, 6.152e0]) * unyt.erg / unyt.s / unyt.cm ** 2 +energy_density = fluxes_iliev / unyt.c +photon_energy = energy_density * boxsize ** 3 / nparts * a_begin ** 3 +photon_energy = photon_energy * f_reduce_speed_of_light + +photon_energy.convert_to_units(cosmo_units["energy"]) +photon_fluxes = 0.333333 * unyt.c * photon_energy +photon_fluxes.convert_to_units( + cosmo_units["energy"] * cosmo_units["length"] / cosmo_units["time"] +) + + +for grp in range(nPhotonGroups): + dsetname = "PhotonEnergiesGroup{0:d}".format(grp + 1) + energydata = np.ones(nparts, dtype=np.float32) * photon_energy[grp] + parts.create_dataset(dsetname, data=energydata) + + dsetname = "PhotonFluxesGroup{0:d}".format(grp + 1) + fluxdata = np.zeros((nparts, 3), dtype=np.float32) + fluxdata[:, 0] = photon_fluxes[grp] + parts.create_dataset(dsetname, data=fluxdata) + +# close up, and we're done! +F.close() diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/plotSolution.py b/examples/RadiativeTransferTests/CosmoHeatingTest/plotSolution.py new file mode 100755 index 0000000000000000000000000000000000000000..c596c24946d0e8e25fb23b18024c9f5290a95155 --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoHeatingTest/plotSolution.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +############################################################################### +# This file is part of SWIFT. +# Copyright (c) 2022 Mladen Ivkovic (mladen.ivkovic@hotmail.com) +# 2024 Stan Verhoeve (s06verhoeve@gmail.com) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +# ------------------------------------------- +# Plot the gas temperature, mean molecular +# weight, and mass fractions +# ------------------------------------------- + +import copy +import os + +import numpy as np +import swiftsimio +import unyt +from matplotlib import pyplot as plt +from matplotlib import ticker as mticker + +# arguments for plots of results +plotkwargs = {"alpha": 0.5} +# arguments for plot of references +referenceplotkwargs = {"color": "grey", "lw": 4, "alpha": 0.6} +# arguments for legends +legendprops = {"size": 8} +# snapshot basenames +snapshot_base = "output" +# Plot time on x-axis +plot_time = False + +# ----------------------------------------------------------------------- + +energy_units = unyt.Msun * unyt.kpc ** 2 / unyt.kyr ** 2 +mass_units = unyt.Msun + + +def mean_molecular_weight(XH0, XHp, XHe0, XHep, XHepp): + """ + Determines the mean molecular weight for given + mass fractions of + hydrogen: XH0 + H+: XHp + He: XHe0 + He+: XHep + He++: XHepp + + returns: + mu: mean molecular weight [in atomic mass units] + NOTE: to get the actual mean mass, you still need + to multiply it by m_u, as is tradition in the formulae + """ + + # 1/mu = sum_j X_j / A_j * (1 + E_j) + # A_H = 1, E_H = 0 + # A_Hp = 1, E_Hp = 1 + # A_He = 4, E_He = 0 + # A_Hep = 4, E_Hep = 1 + # A_Hepp = 4, E_Hepp = 2 + one_over_mu = XH0 + 2 * XHp + 0.25 * XHe0 + 0.5 * XHep + 0.75 * XHepp + + return 1.0 / one_over_mu + + +def gas_temperature(u, mu, gamma): + """ + Compute the gas temperature given the specific internal + energy u and the mean molecular weight mu + """ + + # Using u = 1 / (gamma - 1) * p / rho + # and p = N/V * kT = rho / (mu * m_u) * kT + + T = u * (gamma - 1) * mu * unyt.atomic_mass_unit / unyt.boltzmann_constant + + return T + + +def get_snapshot_list(snapshot_basename="output"): + """ + Find the snapshot(s) that are to be plotted + and return their names as list + """ + + snaplist = [] + + dirlist = os.listdir() + for f in dirlist: + if f.startswith(snapshot_basename) and f.endswith("hdf5"): + snaplist.append(f) + + if len(snaplist) == 0: + raise FileNotFoundError( + "Didn't find any snapshots with basename '" + snapshot_basename + "'" + ) + + snaplist = sorted(snaplist) + + return snaplist + + +def get_ion_mass_fractions(swiftsimio_loaded_data): + """ + Returns the ion mass fractions according to + the used scheme. + + swiftsimio_loaded_data: the swiftsimio.load() object + """ + + data = swiftsimio_loaded_data + meta = data.metadata + try: + scheme = str(meta.subgrid_scheme["RT Scheme"].decode("utf-8")) + except KeyError: + raise ValueError("This test needs to be run with RT on.") + + if scheme.startswith("GEAR M1closure"): + imf = data.gas.ion_mass_fractions + elif scheme.startswith("SPH M1closure"): + # atomic mass + mamu = {"e": 0.0, "HI": 1.0, "HII": 1.0, "HeI": 4.0, "HeII": 4.0, "HeIII": 4.0} + mass_function_hydrogen = data.gas.rt_element_mass_fractions.hydrogen + imf = copy.deepcopy(data.gas.rt_species_abundances) + named_columns = data.gas.rt_species_abundances.named_columns + for column in named_columns: + # abundance is in n_X/n_H unit. We convert it to mass fraction by multipling mass fraction of H + mass_function = ( + getattr(data.gas.rt_species_abundances, column) + * mass_function_hydrogen + * mamu[column] + ) + setattr(imf, column, mass_function) + else: + raise ValueError("Unknown scheme", scheme) + + return imf + + +def get_snapshot_data(snaplist): + """ + Extract the relevant data from the list of snapshots. + + Returns: + numpy arrays of: + time + temperatures + mean molecular weights + mass fractions + """ + + nsnaps = len(snaplist) + firstdata = swiftsimio.load(snaplist[0]) + ngroups = int(firstdata.metadata.subgrid_scheme["PhotonGroupNumber"]) + + scale_factors = np.zeros(nsnaps) + times = np.zeros(nsnaps) * unyt.Myr + temperatures = np.zeros(nsnaps) * unyt.K + mean_molecular_weights = np.zeros(nsnaps) * unyt.atomic_mass_unit + mass_fractions = np.zeros((nsnaps, 5)) + internal_energies = np.zeros(nsnaps) * energy_units + photon_energies = np.zeros((ngroups, nsnaps)) * energy_units + + time_first = firstdata.metadata.time.copy() + for i, snap in enumerate(snaplist): + + data = swiftsimio.load(snap) + gamma = data.gas.metadata.gas_gamma[0] + time = data.metadata.time.copy() + scale_factor = data.metadata.scale_factor + gas = data.gas + + u = gas.internal_energies.to(energy_units / mass_units) + u.convert_to_physical() + u_phys_cgs = u.to(unyt.erg / unyt.g) + masses = gas.masses.to(mass_units) + imf = get_ion_mass_fractions(data) + mu = mean_molecular_weight(imf.HI, imf.HII, imf.HeI, imf.HeII, imf.HeIII) + mu.convert_to_physical() + T = gas_temperature(u_phys_cgs, mu, gamma).to("K") + um = u.to(energy_units / mass_units) * masses + + scale_factors[i] = scale_factor + times[i] = time.to("Myr") + temperatures[i] = np.mean(T) + mean_molecular_weights[i] = np.mean(mu) + internal_energies[i] = np.mean(um) + + mass_fractions[i, 0] = np.mean(imf.HI) + mass_fractions[i, 1] = np.mean(imf.HII) + mass_fractions[i, 2] = np.mean(imf.HeI) + mass_fractions[i, 3] = np.mean(imf.HeII) + mass_fractions[i, 4] = np.mean(imf.HeIII) + + for g in range(ngroups): + en = getattr(data.gas.photon_energies, "group" + str(g + 1)) + en = en.to(energy_units) + photon_energies[g, i] = en.sum() / en.shape[0] + + return ( + scale_factors, + times, + temperatures, + mean_molecular_weights, + mass_fractions, + internal_energies, + photon_energies, + ) + + +if __name__ == "__main__": + # ------------------ + # Plot figures + # ------------------ + + snaplist = get_snapshot_list(snapshot_base) + a, t, T, mu, mass_fraction, u, photon_energies = get_snapshot_data(snaplist) + ngroups = photon_energies.shape[0] + + if plot_time: + xcoords = t + xlabel = "Time [Myr]" + xscale = "log" + else: + xcoords = a + xlabel = "Scale factor [1]" + xscale = "linear" + + fig = plt.figure(figsize=(8, 8), dpi=300) + ax1 = fig.add_subplot(2, 2, 1) + ax2 = fig.add_subplot(2, 2, 2) + ax3 = fig.add_subplot(2, 2, 3) + ax4 = fig.add_subplot(2, 2, 4) + + ax1.semilogy(xcoords, T, label="obtained results") + ax1.set_xlabel(xlabel) + ax1.set_ylabel("gas temperature [K]") + ax1.legend(prop=legendprops) + ax1.grid() + + ax2.plot(xcoords, mu, label="obtained results") + ax2.set_xlabel(xlabel) + ax2.set_ylabel("mean molecular weight") + ax2.legend(prop=legendprops) + ax2.grid() + + total_mass_fraction = np.sum(mass_fraction, axis=1) + ax3.plot(xcoords, total_mass_fraction, "k", label="total", ls="-") + + ax3.plot(xcoords, mass_fraction[:, 0], label="HI", ls=":", **plotkwargs, zorder=1) + ax3.plot(xcoords, mass_fraction[:, 1], label="HII", ls="-.", **plotkwargs, zorder=1) + ax3.plot(xcoords, mass_fraction[:, 2], label="HeI", ls=":", **plotkwargs, zorder=1) + ax3.plot( + xcoords, mass_fraction[:, 3], label="HeII", ls="-.", **plotkwargs, zorder=1 + ) + ax3.plot( + xcoords, mass_fraction[:, 4], label="HeIII", ls="--", **plotkwargs, zorder=1 + ) + ax3.legend(loc="upper right", prop=legendprops) + ax3.set_xlabel(xlabel) + ax3.set_ylabel("gas mass fractions [1]") + ax3.grid() + + tot_energy = u + np.sum(photon_energies, axis=0) + ax4.plot( + xcoords, + tot_energy, + label=f"total energy budget", + color="k", + ls="--", + **plotkwargs, + ) + for g in range(ngroups): + ax4.plot( + xcoords, + photon_energies[g, :], + label=f"photon energies group {g+1}", + **plotkwargs, + ) + ax4.plot(xcoords, u, label=r"gas internal energy", **plotkwargs) + + ax4.set_yscale("log") + ax4.set_xlabel(xlabel) + ax4.set_ylabel( + r"energy budget [$" + energy_units.units.latex_representation() + "$]", + usetex=True, + ) + ax4.legend(prop=legendprops) + ax4.grid() + + for ax in fig.axes: + ax.set_xscale(xscale) + ax.xaxis.set_minor_formatter(mticker.ScalarFormatter()) + + plt.tight_layout() + plt.savefig("heating_test.png") diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/rt_heating_test.yml b/examples/RadiativeTransferTests/CosmoHeatingTest/rt_heating_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..e0b91d8b80f7e86986fbcc6d84ab8895541f8b08 --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoHeatingTest/rt_heating_test.yml @@ -0,0 +1,65 @@ +MetaData: + run_name: Heating Test + +# Define the system of units to use internally. +InternalUnitSystem: + UnitMass_in_cgs: 1.98848e33 # 1 M_sun in grams + UnitLength_in_cgs: 3.08567758e18 # 1 pc in cm + UnitVelocity_in_cgs: 1e5 # 1 km/s in cm/s + UnitCurrent_in_cgs: 1 # Amperes + UnitTemp_in_cgs: 1 # Kelvin + + +# Parameters governing the time integration +TimeIntegration: + max_nr_rt_subcycles: 1 + time_begin: 0. # The starting time of the simulation (in internal units). + dt_min: 1.0208453377e-08 # 0.01 yr + dt_max: 0.0010208453 # 1 kyr + time_end: 4.10084 # 4 Myr + + +# Parameters governing the snapshots +Snapshots: + basename: output # Common part of the name of output files + scale_factor_first: 0.00990099 # Time of the first output (in internal units) + output_list_on: 0 # (Optional) Enable the output list + output_list: snaplist.txt # (Optional) File containing the output times (see documentation in "Parameter File" section) + delta_time: 1.001 + +# Parameters governing the conserved quantities statistics +Statistics: + time_first: 0.00990099 + delta_time: 1.001 + +# Parameters for the hydrodynamics scheme +SPH: + resolution_eta: 1.2348 # Target smoothing length in units of the mean inter-particle separation (1.2348 == 48Ngbs with the cubic spline kernel). + CFL_condition: 0.6 # Courant-Friedrich-Levy condition for time integration. + minimal_temperature: 10. # Kelvin + +# Parameters related to the initial conditions +InitialConditions: + file_name: ./heating_test.hdf5 # The file to read + periodic: 1 # periodic ICs + +GEARRT: + f_reduce_c: 1e-6 # We don't care about the radiation propagation in this test, so let's speed things up + CFL_condition: 0.9 # CFL condition for RT, independent of hydro + photon_groups_Hz: [3.288e15, 5.945e15, 13.157e15] # Lower photon frequency group bin edges in Hz. Needs to have exactly N elements, where N is the configured number of bins --with-RT=GEAR_N + stellar_luminosity_model: const # Which model to use to determine the stellar luminosities. + const_stellar_luminosities_LSol: [1., 1., 1.] # (Conditional) constant star luminosities for each photon frequency group to use if stellar_luminosity_model:const is set, in units of Solar Luminosity. + set_equilibrium_initial_ionization_mass_fractions: 0 # (Optional) set the initial ionization fractions depending on gas temperature assuming ionization equilibrium. + hydrogen_mass_fraction: 0.76 # total hydrogen mass fraction + stellar_spectrum_type: 1 # Which radiation spectrum to use. 0: constant from 0 until some max frequency set by stellar_spectrum_const_max_frequency_Hz. 1: blackbody spectrum. + stellar_spectrum_blackbody_temperature_K: 1.e5 # (Conditional) if stellar_spectrum_type=1, use this temperature (in K) for the blackbody spectrum. + case_B_recombination: 0 # (Optional) use case B recombination interaction rates. + +Cosmology: # Planck13 (EAGLE flavour) + a_begin: 0.00990099 # z~100 + a_end: 0.011 # z~90 + h: 0.6777 + Omega_cdm: 0.2587481 + Omega_lambda: 0.693 + Omega_b: 0.0482519 + diff --git a/examples/RadiativeTransferTests/CosmoHeatingTest/run.sh b/examples/RadiativeTransferTests/CosmoHeatingTest/run.sh new file mode 100755 index 0000000000000000000000000000000000000000..7a0a1e545963188c2bd3bfeaa04b3177e1de39a5 --- /dev/null +++ b/examples/RadiativeTransferTests/CosmoHeatingTest/run.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# make run.sh fail if a subcommand fails +set -e +set -o pipefail + +if [ ! -f ./heating_test.hdf5 ]; then + echo "creating ICs" + python3 makeIC.py +fi + +# Run SWIFT with RT +../../../swift \ + --hydro \ + --cosmology \ + --threads=4 \ + --verbose=0 \ + --radiation \ + --external-gravity \ + --stars \ + --feedback \ + ./rt_heating_test.yml 2>&1 | tee output.log + +python3 plotSolution.py diff --git a/src/engine.c b/src/engine.c index 35d60b0281a8c5b2ab5b26f670bce614a0f5bf81..f3802439661fb4200cbbea050a3303789f249e31 100644 --- a/src/engine.c +++ b/src/engine.c @@ -1931,7 +1931,10 @@ void engine_run_rt_sub_cycles(struct engine *e) { e->max_active_bin_subcycle = get_max_active_bin(e->ti_current_subcycle); e->min_active_bin_subcycle = get_min_active_bin(e->ti_current_subcycle, ti_subcycle_old); - /* TODO: add rt_props_update() for cosmological thermochemistry*/ + + /* Update rt properties */ + rt_props_update(e->rt_props, e->internal_units, e->cosmology); + if (e->policy & engine_policy_cosmology) { double time_old = time; cosmology_update( @@ -2080,6 +2083,9 @@ void engine_init_particles(struct engine *e, int flag_entropy_ICs, cooling_update(e->physical_constants, e->cosmology, e->pressure_floor_props, e->cooling_func, e->s, e->time); + if (e->policy & engine_policy_rt) + rt_props_update(e->rt_props, e->internal_units, e->cosmology); + #ifdef WITH_CSDS if (e->policy & engine_policy_csds) { /* Mark the first time step in the particle csds file. */ @@ -2513,6 +2519,10 @@ int engine_step(struct engine *e) { hydro_props_update(e->hydro_properties, e->gravity_properties, e->cosmology); + /* Update the rt properties */ + if (e->policy & engine_policy_rt) + rt_props_update(e->rt_props, e->internal_units, e->cosmology); + /* Check for any snapshot triggers */ engine_io_check_snapshot_triggers(e); @@ -3995,7 +4005,7 @@ void engine_struct_restore(struct engine *e, FILE *stream) { struct rt_props *rt_properties = (struct rt_props *)malloc(sizeof(struct rt_props)); rt_struct_restore(rt_properties, stream, e->physical_constants, - e->internal_units); + e->internal_units, cosmo); e->rt_props = rt_properties; struct black_holes_props *black_holes_properties = diff --git a/src/hydro/Gizmo/hydro_io.h b/src/hydro/Gizmo/hydro_io.h index 3d2e3cbded1866cf6c94c53aa86d8e8b46a76c2a..25a3d9881dd3d077e67d3d56f5f381a3bd983a8b 100644 --- a/src/hydro/Gizmo/hydro_io.h +++ b/src/hydro/Gizmo/hydro_io.h @@ -216,7 +216,7 @@ INLINE static void hydro_write_particles(const struct part* parts, list[4] = io_make_output_field_convert_part( "InternalEnergies", FLOAT, 1, UNIT_CONV_ENERGY_PER_UNIT_MASS, - 3.f * hydro_gamma_minus_one, parts, xparts, convert_u, + -3.f * hydro_gamma_minus_one, parts, xparts, convert_u, "Co-moving thermal energies per unit mass of the particles"); list[5] = diff --git a/src/rt/GEAR/rt_getters.h b/src/rt/GEAR/rt_getters.h index dc9e3e6b9cb4a5337e357aad05cb4da1976cb4bb..a11ee9cbbecc1d660160d9e2594caf8d9eea8b80 100644 --- a/src/rt/GEAR/rt_getters.h +++ b/src/rt/GEAR/rt_getters.h @@ -29,20 +29,35 @@ */ /** - * @brief Get the radiation energy densities of a particle. + * @brief Get the comoving radiation energy densities of a particle. * * @param p Particle. * @param E (return) Pointer to the array in which the result needs to be stored */ __attribute__((always_inline)) INLINE static void -rt_part_get_radiation_energy_density(const struct part *restrict p, - float E[RT_NGROUPS]) { +rt_part_get_comoving_radiation_energy_density(const struct part *restrict p, + float E[RT_NGROUPS]) { for (int g = 0; g < RT_NGROUPS; g++) { E[g] = p->rt_data.radiation[g].energy_density; } } +/** + * @brief Get the physical radiation energy densities of a particle + * + * @param p Particle. + * @param E (return) Pointer to the array in which the result needs to be stored + */ +__attribute__((always_inline)) INLINE static void +rt_part_get_physical_radiation_energy_density(const struct part *restrict p, + float E[RT_NGROUPS], + const struct cosmology *cosmo) { + for (int g = 0; g < RT_NGROUPS; g++) { + E[g] = cosmo->a3_inv * p->rt_data.radiation[g].energy_density; + } +} + /** * @brief Get a 4-element state vector U containing the radiation energy * density and fluxes for a specific photon group. diff --git a/src/rt/GEAR/rt_grackle_utils.h b/src/rt/GEAR/rt_grackle_utils.h index 599221737365832fc1f8df665fe67757006a93e3..d5b281eabf3095769526798dc3298a855717b5c3 100644 --- a/src/rt/GEAR/rt_grackle_utils.h +++ b/src/rt/GEAR/rt_grackle_utils.h @@ -35,6 +35,21 @@ * @brief Utility and helper functions related to using grackle. */ +/** + * @brief Update grackle units during run + * + * @param grackle_units grackle units struct + * @param cosmo cosmology struct + * + * NOTE: In the current implementation, this function does nothing. + * However, there might be use-cases in the future (e.g. switching + * UV background on or off depending on redshift) that might be + * needed in the future, which can be implemented into this function. + */ +__attribute__((always_inline)) INLINE void update_grackle_units_cosmo( + code_units *grackle_units, const struct unit_system *us, + const struct cosmology *restrict cosmo) {} + /** * @brief initialize grackle during rt_props_init * @@ -50,7 +65,8 @@ __attribute__((always_inline)) INLINE static void rt_init_grackle( code_units *grackle_units, chemistry_data *grackle_chemistry_data, chemistry_data_storage *grackle_chemistry_rates, float hydrogen_mass_fraction, const int grackle_verb, - const int case_B_recombination, const struct unit_system *us) { + const int case_B_recombination, const struct unit_system *us, + const struct cosmology *restrict cosmo) { grackle_verbose = grackle_verb; diff --git a/src/rt/GEAR/rt_properties.h b/src/rt/GEAR/rt_properties.h index 49b0c9c60d50067f7927faa22bcc6df5f549db2d..6f9e95201bc8e145e11afc4c9f4a437814b6f205 100644 --- a/src/rt/GEAR/rt_properties.h +++ b/src/rt/GEAR/rt_properties.h @@ -452,7 +452,7 @@ __attribute__((always_inline)) INLINE static void rt_props_init( params, "GEARRT:case_B_recombination", /*default=*/1); rt_init_grackle(&rtp->grackle_units, &rtp->grackle_chemistry_data, &rtp->grackle_chemistry_rates, rtp->hydrogen_mass_fraction, - rtp->grackle_verbose, rtp->case_B_recombination, us); + rtp->grackle_verbose, rtp->case_B_recombination, us, cosmo); /* Pre-compute interaction rates/cross sections */ /* -------------------------------------------- */ @@ -465,6 +465,12 @@ __attribute__((always_inline)) INLINE static void rt_props_init( /* --------- */ } +__attribute__((always_inline)) INLINE static void rt_props_update( + struct rt_props* rtp, const struct unit_system* us, + struct cosmology* cosmo) { + update_grackle_units_cosmo(&(rtp->grackle_units), us, cosmo); +} + /** * @brief Write an RT properties struct to the given FILE as a * stream of bytes. @@ -491,10 +497,11 @@ __attribute__((always_inline)) INLINE static void rt_struct_dump( * @param stream the file stream * @param phys_const The physical constants in the internal unit system. * @param us The internal unit system. + * @param cosmo the #cosmology */ __attribute__((always_inline)) INLINE static void rt_struct_restore( struct rt_props* props, FILE* stream, const struct phys_const* phys_const, - const struct unit_system* us) { + const struct unit_system* us, const struct cosmology* restrict cosmo) { restart_read_blocks((void*)props, sizeof(struct rt_props), 1, stream, NULL, "RT properties struct"); @@ -502,7 +509,7 @@ __attribute__((always_inline)) INLINE static void rt_struct_restore( rt_init_grackle(&props->grackle_units, &props->grackle_chemistry_data, &props->grackle_chemistry_rates, props->hydrogen_mass_fraction, props->grackle_verbose, - props->case_B_recombination, us); + props->case_B_recombination, us, cosmo); props->energy_weighted_cross_sections = NULL; props->number_weighted_cross_sections = NULL; diff --git a/src/rt/GEAR/rt_thermochemistry.h b/src/rt/GEAR/rt_thermochemistry.h index 40aafd2b91f7e7f7c4a9a628d93c3b354bbcc352..a473f180393e601534c4225d8d18b72db62faf72 100644 --- a/src/rt/GEAR/rt_thermochemistry.h +++ b/src/rt/GEAR/rt_thermochemistry.h @@ -122,7 +122,6 @@ INLINE static void rt_do_thermochemistry( grackle_field_data particle_grackle_data; gr_float density = hydro_get_physical_density(p, cosmo); - /* In rare cases, unphysical solutions can arise with negative densities * which won't be fixed in the hydro part until further down the dependency * graph. Also, we can have vacuum, in which case we have nothing to do here. @@ -130,15 +129,20 @@ INLINE static void rt_do_thermochemistry( if (density <= 0.) return; const float u_minimal = hydro_props->minimal_internal_energy; - gr_float internal_energy = - max(hydro_get_physical_internal_energy(p, xp, cosmo), u_minimal); + + /* Physical internal energy */ + gr_float internal_energy_phys = + hydro_get_physical_internal_energy(p, xp, cosmo); + gr_float internal_energy = max(internal_energy_phys, u_minimal); + const float u_old = internal_energy; gr_float species_densities[6]; rt_tchem_get_species_densities(p, density, species_densities); float radiation_energy_density[RT_NGROUPS]; - rt_part_get_radiation_energy_density(p, radiation_energy_density); + rt_part_get_physical_radiation_energy_density(p, radiation_energy_density, + cosmo); gr_float iact_rates[5]; rt_get_interaction_rates_for_grackle( @@ -152,9 +156,6 @@ INLINE static void rt_do_thermochemistry( iact_rates); /* solve chemistry */ - /* Note: `grackle_rates` is a global variable defined by grackle itself. - * Using a manually allocd and initialized variable here fails with MPI - * for some reason. */ if (local_solve_chemistry( &rt_props->grackle_chemistry_data, &rt_props->grackle_chemistry_rates, &rt_props->grackle_units, &particle_grackle_data, dt) == 0) @@ -163,8 +164,9 @@ INLINE static void rt_do_thermochemistry( /* copy updated grackle data to particle */ /* update particle internal energy. Grackle had access by reference * to internal_energy */ - internal_energy = particle_grackle_data.internal_energy[0]; - const float u_new = max(internal_energy, u_minimal); + internal_energy_phys = particle_grackle_data.internal_energy[0]; + + const float u_new = max(internal_energy_phys, u_minimal); /* Re-do thermochemistry? */ if ((rt_props->max_tchem_recursion > depth) && @@ -180,7 +182,7 @@ INLINE static void rt_do_thermochemistry( } /* If we're good, update the particle data from grackle results */ - hydro_set_internal_energy(p, u_new); + hydro_set_physical_internal_energy(p, xp, cosmo, u_new); /* Update mass fractions */ const gr_float one_over_rho = 1. / density; @@ -232,7 +234,6 @@ INLINE static void rt_do_thermochemistry( p->rt_data.radiation[g].flux, E_old, /*callloc=*/2); } - /* Clean up after yourself. */ rt_clean_grackle_fields(&particle_grackle_data); } @@ -262,14 +263,18 @@ __attribute__((always_inline)) INLINE static float rt_tchem_get_tchem_time( gr_float density = hydro_get_physical_density(p, cosmo); const float u_minimal = hydro_props->minimal_internal_energy; - gr_float internal_energy = - max(hydro_get_physical_internal_energy(p, xp, cosmo), u_minimal); + + /* Physical internal energy */ + gr_float internal_energy_phys = + hydro_get_physical_internal_energy(p, xp, cosmo); + gr_float internal_energy = max(internal_energy_phys, u_minimal); gr_float species_densities[6]; rt_tchem_get_species_densities(p, density, species_densities); float radiation_energy_density[RT_NGROUPS]; - rt_part_get_radiation_energy_density(p, radiation_energy_density); + rt_part_get_physical_radiation_energy_density(p, radiation_energy_density, + cosmo); gr_float iact_rates[5]; rt_get_interaction_rates_for_grackle( @@ -282,15 +287,11 @@ __attribute__((always_inline)) INLINE static float rt_tchem_get_tchem_time( iact_rates); /* Compute 'cooling' time */ - /* Note: grackle_rates is a global variable defined by grackle itself. - * Using a manually allocd and initialized variable here fails with MPI - * for some reason. */ gr_float tchem_time; if (local_calculate_cooling_time( &rt_props->grackle_chemistry_data, &rt_props->grackle_chemistry_rates, &rt_props->grackle_units, &particle_grackle_data, &tchem_time) == 0) error("Error in calculate_cooling_time."); - /* Clean up after yourself. */ rt_clean_grackle_fields(&particle_grackle_data); diff --git a/src/rt/SPHM1RT/rt_properties.h b/src/rt/SPHM1RT/rt_properties.h index f2eb53d2030fb103296dd691fa3e207ac9fd2423..38dbde050a35a00a33eae3ef1f59637ca549ce49 100644 --- a/src/rt/SPHM1RT/rt_properties.h +++ b/src/rt/SPHM1RT/rt_properties.h @@ -402,6 +402,9 @@ __attribute__((always_inline)) INLINE static void rt_props_init( message("Radiative transfer initialized"); } } +__attribute__((always_inline)) INLINE static void rt_props_update( + struct rt_props* rtp, const struct unit_system* us, + struct cosmology* cosmo) {} /** * @brief Write an RT properties struct to the given FILE as a @@ -428,7 +431,7 @@ __attribute__((always_inline)) INLINE static void rt_struct_dump( */ __attribute__((always_inline)) INLINE static void rt_struct_restore( struct rt_props* props, FILE* stream, const struct phys_const* phys_const, - const struct unit_system* us) { + const struct unit_system* us, const struct cosmology* restrict cosmo) { restart_read_blocks((void*)props, sizeof(struct rt_props), 1, stream, NULL, "RT properties struct"); diff --git a/src/rt/debug/rt_properties.h b/src/rt/debug/rt_properties.h index 9fba0a49b274eb4499c31a6072032d92a15f45c8..d4a31a2c1b3c91537b3538f120bc1373ce41a0a8 100644 --- a/src/rt/debug/rt_properties.h +++ b/src/rt/debug/rt_properties.h @@ -92,6 +92,10 @@ __attribute__((always_inline)) INLINE static void rt_props_init( parser_get_param_int(params, "TimeIntegration:max_nr_rt_subcycles"); } +__attribute__((always_inline)) INLINE static void rt_props_update( + struct rt_props* rtp, const struct unit_system* us, + struct cosmology* cosmo) {} + /** * @brief Write an RT properties struct to the given FILE as a * stream of bytes. @@ -117,7 +121,7 @@ __attribute__((always_inline)) INLINE static void rt_struct_dump( */ __attribute__((always_inline)) INLINE static void rt_struct_restore( struct rt_props* props, FILE* stream, const struct phys_const* phys_const, - const struct unit_system* us) { + const struct unit_system* us, const struct cosmology* restrict cosmo) { restart_read_blocks((void*)props, sizeof(struct rt_props), 1, stream, NULL, "RT properties struct"); diff --git a/src/rt/none/rt_properties.h b/src/rt/none/rt_properties.h index 16fd5fa5bf64987b7203e5437fa2da1ed1844aee..07e3cef313c2176b1d81f4a089472bd0b7c07f84 100644 --- a/src/rt/none/rt_properties.h +++ b/src/rt/none/rt_properties.h @@ -59,6 +59,10 @@ __attribute__((always_inline)) INLINE static void rt_props_init( const struct unit_system* us, struct swift_params* params, struct cosmology* cosmo) {} +__attribute__((always_inline)) INLINE static void rt_props_update( + struct rt_props* rtp, const struct unit_system* us, + struct cosmology* cosmo) {} + /** * @brief Write an RT properties struct to the given FILE as a * stream of bytes. @@ -84,7 +88,7 @@ __attribute__((always_inline)) INLINE static void rt_struct_dump( */ __attribute__((always_inline)) INLINE static void rt_struct_restore( struct rt_props* props, FILE* stream, const struct phys_const* phys_const, - const struct unit_system* us) { + const struct unit_system* us, const struct cosmology* restrict cosmo) { restart_read_blocks((void*)props, sizeof(struct rt_props), 1, stream, NULL, "RT properties struct");