Defining Simulation Inputs¶
In this tutorial, we’ll cover how to setup your simulation by defining the basic simulation and physical parameters.
[26]:
import py21cmfast as p21c
from tempfile import mkdtemp
from pathlib import Path
[3]:
print(f' Using 21cmFAST version {p21c.__version__}')
Using 21cmFAST version 4.0.0b1.dev312+gb6f5204f.d20250728
The InputParameters Class and Parameter Subgroups¶
All the parameters that 21cmFAST uses are stored in the InputParameters class. This class handles the validation of the set of parameters you specify (in case there are conflicts between parameters), and also gives you a few ways to setup the parameters.
The easiest way is to use all defaults:
[4]:
inputs = p21c.InputParameters(random_seed=1234)
Note
Why do we require you to specify the random seed explicitly? Because doing so minimizes surprises. 21cmFAST attempts to cache results, and can therefore return the same simulation when the same parameters are given. This is sometimes surprising, if you were trying to generate multiple realizations of simulations with the same parameters. Always explicitly specifying the seed requires you to take control of this behavior.
You will see that within the InputParameters object, the actual parameters are divvied up into multiple sub-categories:
[5]:
print(inputs)
cosmo_params: CosmoParams(SIGMA_8=0.8102, hlittle=0.6766, OMm=0.30964144154550644, OMb=0.04897468161869667, POWER_INDEX=0.9665, OMn=0.0, OMk=0.0, OMr=8.6e-05, OMtot=1.0, Y_He=0.24, wl=-1.0)
simulation_options: SimulationOptions(HII_DIM=256, _BOX_LEN=None, _DIM=None, _HIRES_TO_LOWRES_FACTOR=None, _LOWRES_CELL_SIZE_MPC=None, NON_CUBIC_FACTOR=1.0, N_THREADS=1, SAMPLER_MIN_MASS=100000000.0, SAMPLER_BUFFER_FACTOR=2.0, N_COND_INTERP=200, N_PROB_INTERP=400, MIN_LOGPROB=-12.0, HALOMASS_CORRECTION=0.89, PARKINSON_G0=1.0, PARKINSON_y1=0.0, PARKINSON_y2=0.0, Z_HEAT_MAX=35.0, ZPRIME_STEP_FACTOR=1.02, INITIAL_REDSHIFT=300.0, DELTA_R_FACTOR=1.1, DENSITY_SMOOTH_RADIUS=0.2, DEXM_OPTIMIZE_MINMASS=100000000000.0, DEXM_R_OVERLAP=2.0, CORR_STAR=0.5, CORR_SFR=0.2, CORR_LX=0.2)
matter_options: MatterOptions(HMF='ST', USE_RELATIVE_VELOCITIES=False, POWER_SPECTRUM='EH', PERTURB_ON_HIGH_RES=False, USE_INTERPOLATION_TABLES='hmf-interpolation', MINIMIZE_MEMORY=False, KEEP_3D_VELOCITIES=False, SAMPLE_METHOD='MASS-LIMITED', FILTER='spherical-tophat', HALO_FILTER='spherical-tophat', SMOOTH_EVOLVED_DENSITY_FIELD=False, DEXM_OPTIMIZE=False, PERTURB_ALGORITHM='2LPT', USE_FFTW_WISDOM=False, USE_HALO_FIELD=True, FIXED_HALO_GRIDS=False, HALO_STOCHASTICITY=True)
astro_params: AstroParams(HII_EFF_FACTOR=30.0, F_STAR10=-1.3, ALPHA_STAR=0.5, F_STAR7_MINI=-2.8, ALPHA_STAR_MINI=0.5, F_ESC10=-1.0, ALPHA_ESC=-0.5, F_ESC7_MINI=-2.0, M_TURN=8.7, R_BUBBLE_MAX=15.0, R_BUBBLE_MIN=0.620350491, ION_Tvir_MIN=4.69897, L_X=40.5, L_X_MINI=40.5, NU_X_THRESH=500.0, X_RAY_SPEC_INDEX=1.0, X_RAY_Tvir_MIN=4.69897, F_H2_SHIELD=0.0, t_STAR=0.5, A_LW=2.0, BETA_LW=0.6, A_VCB=1.0, BETA_VCB=1.8, UPPER_STELLAR_TURNOVER_MASS=11.447, UPPER_STELLAR_TURNOVER_INDEX=-0.6, SIGMA_STAR=0.25, SIGMA_LX=0.5, SIGMA_SFR_LIM=0.19, SIGMA_SFR_INDEX=-0.12, T_RE=20000.0, FIXED_VAVG=25.86, POP2_ION=5000.0, POP3_ION=44021.0, PHOTONCONS_CALIBRATION_END=3.5, CLUMPING_FACTOR=2.0, ALPHA_UVB=5.0, R_MAX_TS=500.0, N_STEP_TS=40, MAX_DVDR=0.2, DELTA_R_HII_FACTOR=1.1, NU_X_BAND_MAX=2000.0, NU_X_MAX=10000.0)
astro_options: AstroOptions(USE_MINI_HALOS=False, USE_CMB_HEATING=True, USE_LYA_HEATING=True, USE_MASS_DEPENDENT_ZETA=True, INHOMO_RECO=False, USE_TS_FLUCT=False, FIX_VCB_AVG=False, USE_EXP_FILTER=True, CELL_RECOMB=True, PHOTON_CONS_TYPE='no-photoncons', USE_UPPER_STELLAR_TURNOVER=True, M_MIN_in_Mass=True, HALO_SCALING_RELATIONS_MEDIAN=False, HII_FILTER='spherical-tophat', HEAT_FILTER='spherical-tophat', IONISE_ENTIRE_SPHERE=False, AVG_BELOW_SAMPLER=True, INTEGRATION_METHOD_ATOMIC='GAUSS-LEGENDRE', INTEGRATION_METHOD_MINI='GAUSS-LEGENDRE')
These five categories (CosmoParams, SimulationOptions, MatterOptions, AstroParams and AstroOptions) come in two types: options and params. Here options refers to choices that affect how the simulation is run, for exapmle the size of the grid, or whether to run with certain physical models. These are generally integers or booleans (though they can be floats as well sometimes). On the other hand, params specify physical parameters of the simulation, for instance
cosmological parameters like \(\sigma_8\), or astrophysical parameters like \(f_{\rm esc}\).
The reason we break them into these groups—besides the conceptual benefit—is that it is then easier to identify parameters that might be constrained by data (via MCMC for example).
Within a type, the different parameter groups (e.g. SimulationOptions, MatterOptions and AstroOptions) affect different stages of the simulation, and are broken up to enable better caching.
Manually Specifying Parameters¶
One way to specify an InputParameters class is to build it from individual subgroups. For example:
[6]:
inputs = p21c.InputParameters(
cosmo_params = p21c.CosmoParams(SIGMA_8=0.6),
astro_options = p21c.AstroOptions(USE_TS_FLUCT=True),
random_seed=1234
)
In this case, any subgroup that is not specified will be filled with all default parameters (see the API reference to check what these are).
Furthermore, any parameter within any subgroup that is not specified will receive its default.
Warning
Specifying some parameters as non-default values without explicitly changing other parameters can result in validation errors. 21cmFAST doesn’t generally provide dynamic defaults because there are too many situations to cover and too many surprises for you. Instead, it will raise exceptions and inform you how to fix your inputs. This can be made easier by using templates. Read on!
Evolving Existing Parameters¶
In some cases, you have an existing set of parameters (for instance, you may have read the input parameters from an existing simulation file) and you want to create a new set of parameters based on these parameters. In this case you cannot update the existing parameters in-place:
[7]:
try:
inputs.simulation_options.SIGMA_8 = 0.9
except Exception as e:
print(type(e))
<class 'attr.exceptions.FrozenInstanceError'>
This highlights a key point of the InputParameters class: it is immutable. Once you have your parameters, you can be confident that they’re not going to be secretly modified under the hood.
Instead, you can use one set of parameters to define a new set, like so:
[8]:
new_inputs = inputs.evolve_input_structs(SIGMA_8=0.9)
[9]:
print("New Sigma_8: ", new_inputs.cosmo_params.SIGMA_8)
print("Original Sigma_8: ", inputs.cosmo_params.SIGMA_8)
New Sigma_8: 0.9
Original Sigma_8: 0.6
Note that the evolve_input_structs method directly accepts parameters from any of the subgroups, without having to specify which subgroup it comes from. If you would instead prefer to update an entire subgroup at a time, use the .clone() method:
[10]:
new = inputs.clone(cosmo_params=p21c.CosmoParams(hlittle=0.9))
Using Templates¶
The recommended way to specify your parameters is by starting from a built-in template. The reason for this is that there are many “modes” in which to run 21cmFAST (and the number grows over time), and each mode may require several parameters to be specified. Instead of having to know and remember the full set of interlocking parameters, we provide templates that give you the basics, upon which you can build.
To build from a template, simply use the .from_template constructor method:
[11]:
inputs = p21c.InputParameters.from_template("Park19", random_seed=1)
[12]:
print(inputs.astro_options)
AstroOptions:AVG_BELOW_SAMPLER : True
CELL_RECOMB : False
FIX_VCB_AVG : False
HALO_SCALING_RELATIONS_MEDIAN: False
HEAT_FILTER : spherical-tophat
HII_FILTER : sharp-k
INHOMO_RECO : True
INTEGRATION_METHOD_ATOMIC : GAUSS-LEGENDRE
INTEGRATION_METHOD_MINI : GAUSS-LEGENDRE
IONISE_ENTIRE_SPHERE : False
M_MIN_in_Mass : True
PHOTON_CONS_TYPE : no-photoncons
USE_CMB_HEATING : False
USE_EXP_FILTER : False
USE_LYA_HEATING : False
USE_MASS_DEPENDENT_ZETA : True
USE_MINI_HALOS : False
USE_TS_FLUCT : True
USE_UPPER_STELLAR_TURNOVER : False
You can overwrite specific parameters by passing them to the constructor:
[ ]:
inputs = p21c.InputParameters.from_template(
"Park19", USE_MINI_HALOS=True, random_seed=1
)
/data4/smurray/miniforge3/envs/21cmfast/lib/python3.11/site-packages/attr/_make.py:3040: UserWarning: USE_MINI_HALOS needs USE_RELATIVE_VELOCITIES to get the right evolution!
v(inst, attr, value)
/data4/smurray/miniforge3/envs/21cmfast/lib/python3.11/site-packages/attr/_make.py:3040: UserWarning: You are setting M_TURN > 8 when USE_MINI_HALOS=True. This is non-standard (but allowed), and usually occurs upon manual update of M_TURN
v(inst, attr, value)
As you can see, overwriting this particular option – USE_MINI_HALOS – on its own has generated some warnings, because in fact a model using mini-halos requires other options to be set in order to be accurate. This is an example of why building from a template is easier. We could have used the EOS21 template, which instantiates the basic model presented in https://arxiv.org/abs/2110.13919, which includes molecularly-cooled galaxies that form in mini-halos:
[14]:
inputs = p21c.InputParameters.from_template("EOS21", random_seed=1)
[15]:
print("EOS21 uses minihalos?", inputs.astro_options.USE_MINI_HALOS)
EOS21 uses minihalos? True
Stacking Multiple Templates¶
Most built-in templates instantiate a “mode” of running 21cmFAST – some set of options that define the physical models that are used in the simulation – often representing a specific publication or publicly available simulation.
However, another use-case of templates is to quickly set a “size” of your simulation. Often you want to switch between something very small for testing, and something larger when want to produce scientific results. To make this easier, 21cmFAST allows you to stack templates, and provides several built-in templates that only modify simulation size.
For example:
[16]:
inputs_small = p21c.InputParameters.from_template(['EOS21', 'small'], random_seed=1)
inputs_large = p21c.InputParameters.from_template(['EOS21', 'gpc'], random_seed=1)
/data4/smurray/miniforge3/envs/21cmfast/lib/python3.11/site-packages/attr/_make.py:3040: UserWarning: You are setting R_BUBBLE_MAX != 50 when INHOMO_RECO=True. This is non-standard (but allowed), and usually occurs upon manual update of INHOMO_RECO
v(inst, attr, value)
Here, the “small” template combined with EOS21 emits a warning, because the box is too small to set R_BUBBLE_MAX to 50. That is ostensibly OK because this box would only be used for testing anyway.
Listing Available Templates¶
So, what templates are available to you as builtins? You can print a list of available templates most easily from the CLI interface:
[17]:
! 21cmfast template avail
defaults | default
All the default parameters of 21cmFAST.
simple
A simple 21cmFAST run with no minihalos, discrete halos, recombinations
or spin temperature fluctuations
const-zeta
A 21cmFAST run with constant ionising efficiency for halos of all mass
latest
Our latest fiducial run without discrete halos, includes recominations
and spin temperature fluctuations
minihalos | mini
A run including minihalos
latest-discrete | latest-dhalos
Our latest fiducial run with discrete halos, recominations and spin
temperature fluctuations
minihalos-discrete | mini-dhalos
Run with discrete halos, including the molecularly cooled galaxy
component
Park19
Exact fiducial parameters from Park et al 2019. Disables modules
implemented afterwards
Qin20
Exact fiducial parameters from Qin et al 2020. Disables modules
implemented afterwards
Munoz21 | EOS21
Exact fiducial parameters from Munoz et al 2021. Disables modules
implemented afterwards
fixed-halos
integrals of the CHMF on the Eulerian (evolved) density grid, FFRT-P in
Davies+22 or ESF-E in Trac+22
size-tiny | tiny
Set the simulation to have a very small size (48 Mpc), useful for quick
tests. Intended to be used on top of another template
size-small | small
Set the simulation to have a small size (92 Mpc), useful for quick
exploration. Intended to be used on top of another template
size-medium | medium
Set the simulation to have a medium size (384 Mpc), useful for MCMC.
Intended to be used on top of another template.
size-gpc | gpc | large
Set the simulation to have a large size (~1 Gpc). Intended to be used on
top of another template.
This output lists the name (and aliases, which will work just as well as the name when specifying the template) as well as a short description of each built-in template. Templates whose names begin with size- only contain options that modify the size of the simulation (which may include the number of node redshifts).
Generating Required Citations/Acknowledgments¶
You may wonder what papers to cite for a given model that you are using. This is made easier by using the show_references function:
[22]:
p21c.utils.show_references(p21c.InputParameters.from_template('simple', random_seed=0));
The main reference for 21cmFAST v3+:
====================================
Murray et al., (2020). 21cmFAST v3: A Python-integrated C code for generating 3D
realizations of the cosmic 21cm signal. Journal of Open Source Software, 5(54),
2582, https://doi.org/10.21105/joss.02582
Based on the original 21cmFAST model:
=====================================
Andrei Mesinger, Steven Furlanetto and Renyue Cen, "21CMFAST: a fast, seminumerical
simulation of the high-redshift 21-cm signal", Monthly Notices of the Royal
Astronomical Society, Volume 411, Issue 2, pp. 955-972 (2011),
https://ui.adsabs.harvard.edu/link_gateway/2011MNRAS.411..955M/doi:10.1111/j.1365-2966.2010.17731.x
The mass-dependent ionising efficiency model:
=============================================
Park, J., Mesinger, A., Greig, B., and Gillet, N.,
“Inferring the astrophysics of reionization and cosmic dawn from galaxy luminosity
functions and the 21-cm signal”, Monthly Notices of the Royal Astronomical Society,
vol. 484, no. 1, pp. 933–949, 2019. https://doi.org/10.1093/mnras/stz032.
This will print only the references applicable to the set of models switched on according to your InputParameters instance.
Create Your Own Template¶
The built-in templates are simply defined as TOML files. You can create your own Parameter TOML as well:
[27]:
inputs = p21c.InputParameters.from_template(['simple', 'large'], random_seed=1)
# For the sake of the tutorial, write the custom toml into a temporary directory
tomlfile = Path(mkdtemp()) / 'custom_parameters.toml'
p21c.write_template(inputs, tomlfile)
You can then use this TOML file in the .from_template function:
[30]:
inputs_read = p21c.InputParameters.from_template(tomlfile, random_seed=1)
print("Original == New? ", inputs==inputs_read)
Original == New? True
Note
It is not possible to register new ‘built-in’ templates that are accessible by name, only to define new TOML files that can be explicitly pointed to. However, if you have a set of parameters that correspond to a publicly-available simulation or publication of interest, feel free to make a PR that adds it as a built-in!
By default, the write_template function creates a full TOML file with all of the possible parameters specified in it. In fact, you can create a TOML file listing all of the default parameters very easily:
[32]:
inputs = p21c.InputParameters.from_template(['defaults'], random_seed=1)
# For the sake of the tutorial, write the custom toml into a temporary directory
tomlfile = Path(mkdtemp()) / 'default_parameters.toml'
p21c.write_template(inputs, tomlfile)
Sometimes, it’s useful to instead only list the parameters that are non-default, in order to bring attention to what you intend to modify. To do this, set the mode parameter:
[33]:
inputs = p21c.InputParameters.from_template(['defaults'], SIGMA_8=0.9, random_seed=1)
# For the sake of the tutorial, write the custom toml into a temporary directory
tomlfile = Path(mkdtemp()) / 'minimal_parameters.toml'
p21c.write_template(inputs, tomlfile, mode='minimal')
This file should only list the SIGMA_8 parameter:
[34]:
with tomlfile.open('r') as fl:
print(fl.read())
# This file was generated by py21cmfast.run_templates.write_template
# Created on: 2025-07-29T02:21:52.467573
[CosmoParams]
SIGMA_8 = 0.9
Specifying Node Redshifts¶
There is another input that is not captured by the parameter groups listed above, which is the set of redshifts at which the “evolution” of the model is evaluated.
These redshifts are known as the node_redshifts.
Warning
The node_redshifts are a separate concept from the redshifts at which you might want to generate your simulation boxes. The “output” redshifts are different depending on whether you are interesting in coeval boxes or lightcones. In either case, the node_redshifts can in general be different from the output redshifts (within some limitations). The node_redshifts can be thought of as defining the intrinsic evolution of the simulation, while the output redshifts are merely for
viewing.
Not all physical models in 21cmFAST require evolution: the simplest models can be directly evaluated at any redshift. In these cases, while node_redshifts may be specified, it is not required (and by default will not be specified). To check if your model requires node_redshifts, use the evolution_required attribute:
[37]:
simple = p21c.InputParameters.from_template('simple', random_seed=1)
print("Evolution Required for Simple? ", simple.evolution_required)
eos21 = p21c.InputParameters.from_template('EOS21', random_seed=1)
print("Evolution Required for EOS21? ", eos21.evolution_required)
Evolution Required for Simple? False
Evolution Required for EOS21? True
In 21cmFAST, the node_redshifts are completely flexible – you can specify any set of node_redshifts that you like, so long as they are monotonically decreasing, and start above AstroOptions.Z_HEAT_MAX.
However, the default – when the model requires it – is to use a regular log-spaced (in \(1 + z\)) sequence of redshifts, spanning from \(z=5.5\) to $z \ge `$ ``Z_HEAT_MAX` with a geometric spacing of AstroOptions.ZPRIME_STEP_FACTOR.
To control the set of node redshifts, you can pass them explicitly:
[38]:
inputs = p21c.InputParameters.from_template(
"EOS21",
node_redshifts=[7.0, 8.0, 10.0, 15.0, 25.0, 40.0],
random_seed=1
)
However, it is generally better to use log-spaced redshifts. If you do not require control of the minimum redshift, then this can be specified explicitly via normal parameters:
[42]:
inputs_coarse = p21c.InputParameters.from_template(
"EOS21",
ZPRIME_STEP_FACTOR=1.2, # default is 1.02
Z_HEAT_MAX=20.0, # default is 35
random_seed=1
)
[43]:
print(inputs_coarse.node_redshifts)
(22.290675199999992, 18.408895999999995, 15.174079999999996, 12.478399999999999, 10.232, 8.36, 6.8, 5.5)
If you also require control of the minimum redshift, use the .with_logspaced_redshifts constructor method:
[47]:
inputs_coarse_low_zmin = inputs_coarse.with_logspaced_redshifts(zmin=5.0)
print(", ".join(f'{z:.3f}' for z in inputs_coarse_low_zmin.node_redshifts))
20.499, 16.916, 13.930, 11.442, 9.368, 7.640, 6.200, 5.000
The .with_logspaced_redshifts method by default uses the Z_HEAT_MAX and ZPRIME_STEP_FACTOR parameters to set the range and resolution of the grid, but these can be specified manually as well:
[48]:
inputs_coarse_low_zmin = inputs_coarse.with_logspaced_redshifts(zmin=5.0, zmax=21.0, zstep_factor=1.3)
print(", ".join(f'{z:.3f}' for z in inputs_coarse_low_zmin.node_redshifts))
21.278, 16.137, 12.182, 9.140, 6.800, 5.000
Tips on Specifying Simulation Size¶
The “size” of a simulation depends on a number of options, but the main ones are:
DIM: The dimensionality of the high-resolution periodic boxes produced by the initial conditions simulation step.HII_DIM: The dimensionality of the low-resolution periodic boxes produced by all subsequent steps after the initial conditions. While these are lower resolution, there are many more fields that are produced at this dimensionality. Typically this will be about 1/3 or 1/4 ofDIM. Roughly speaking, simulation time, memory and storage scales asHII_DIM^3.node_redshifts: For models that require evolution, the total time and storage requirements scale linearly with the number of node redshifts (see above). Given that these are by default set viaZPRIME_STEP_FACTORandZ_HEAT_MAX, these parameters can be considered as options that affect the size of the simulation.N_STEPS_TS: for models withUSE_TS_FLUCT=True, this parameter controls how many radial shells are computed. The higher the number, the more accurate the results, but the total simulation time and peak memory will scale linearly withN_STEPS_TS(not counting overheads).
The easiest way to switch between different simulation sizes is by using the built-in size-templates. These set multiple size-related parameters at once, with options of tiny, small, medium and large.
From v4, it is recommended to use resolution parameters instead of explicit dimensionalities when setting up boxes. Thus, instead of specifying DIM, specify HIRES_TO_LOWRES_FACTOR along with HII_DIM. And instead of specifying BOX_LEN, specify LOWRES_CELL_SIZE_MPC. Specifying these ratios means that updating HII_DIM will automatically yield a new simulation size with the same resolution (auto-updating DIM and BOX_LEN).
All the built-in size-templates are implemented using ratios, so doing:
[50]:
inputs = p21c.InputParameters.from_template(
['EOS21', 'medium'], random_seed=1
).evolve_input_structs(
HII_DIM=1000
)
will still yield a reasonable box size and high-resolution dimensionality:
[51]:
print("New BOX_LEN: ", inputs.simulation_options.BOX_LEN)
print("New DIM: ", inputs.simulation_options.DIM)
New BOX_LEN: 1500
New DIM: 3000
You can also update the ratios consistently:
[52]:
inputs = inputs.evolve_input_structs(HIRES_TO_LOWRES_FACTOR=4)
print(inputs.simulation_options.DIM)
4000
However, you cannot go from “ratio” mode to “explicit” mode:
[53]:
try:
inputs.evolve_input_structs(DIM=2345)
except Exception as e:
print(type(e), e)
<class 'ValueError'> Cannot set both DIM and HIRES_TO_LOWRES_FACTOR! If this error arose when lading from template/file or evolving an existing object, then explicitly set either DIM or HIRES_TO_LOWRES_FACTOR to None while setting the other to the desired value.
If you must do this, explicitly set the ratio to None:
[54]:
new = inputs.evolve_input_structs(DIM=2345, HIRES_TO_LOWRES_FACTOR=None)