Parameters
The following sections list the various parameters accepted by CONCEPT, along with explanations, default and example values. Each section deals with a specific category of parameters.
To learn how to use parameters and parameter files, see the
tutorial as well as the -p
and -c
command-line options.
Parameters are specified as Python 3 variables. As such, it is helpful to be
familiar with basic Python syntax and
data types, such as
str
ings, list
s and dict
ionaries.
Below you will find the parameter categories, corresponding to the sections ahead. Besides bringing some organization to the large set of parameters, these categories have no meaning.
Besides these sections, you may also look in the
provided example parameter files.
In particular, example_explanatory
defines close to all supported
parameters. Though this makes it impractically large for actual use, it serves
to briefly showcase the many possible parameters.
The rest of this page contains information regarding parameter specification generally, independent of the specific parameters.
Parameter files as Python scripts
You may think of parameter files as (glorified) Python scripts, and use any
valid Python 3 statements to formulate or compute your parameters,
including print()
for debugging.
Even without any explicit import
statement present in the parameter file,
all of numpy
is readily available. As an example, consider the following
specification of 7 power spectrum outputs placed logarithmically equidistant
in scale factor \(a\), from \(a = 0.01\) to \(a = 1\):
# Input/output
output_times = {'powerspec': logspace(log10(0.01), log10(1), 7)}
Units and constants
For dimensional parameters, it is important to explicitly tack the unit onto the value, as no default units are ever assumed by CONCEPT. For example,
# Numerics
boxsize = 512 # ❌
boxsize = 512*Mpc # OK
# Cosmology
H0 = 67 # ❌
H0 = 67*km/(s*Mpc) # OK
A large set of units is understood:
[mck]m
(meter),AU
(astronomical unit),[kMG]ly
(light-year),[kMG]pc
(parsec).s
(second),minutes
,hr
(hour),day
(24 hours),[kMG]yr
(Julian year).[k]g
(gram),[kMG]m☉
or[kMG]m_sun
(solar mass).[mkMGT]eV
(electron volt),J
(Joule).
with brackets [...]
indicating allowed SI prefixes.
CONCEPT always expresses angles in terms of radians.
The following physical constants are further available:
c
orlight_speed
(speed of light in vacuum).G
orG_Newton
(gravitational constant).ħ
orh_bar
(reduced Planck constant).
Note
The precise relations between the above units and constants are adopted from the 2019 redefinition of the SI base units, exact IAU definitions and experimental 2020 values from the Particle Data Group.
A few mathematical constants are similarly readily available:
π
orpi
(half-circle constant).τ
ortau
(circle constant).e
(Euler’s number).ထ
orinf
(infinity).machine_ϵ
oreps
(double-precision machine epsilon).
Tip
For more exotic mathematical constants you are encouraged to make use of NumPy/SciPy:
import scipy.special
scipy.special.zeta(3) # Riemann ζ(3)
Note
The actual numerical value resulting from e.g. 512*Mpc
depends on the
internal unit system employed by CONCEPT. The unit system itself is
not statically defined, but specified on a per-simulation basis via
parameters. Though the vast majority of users have no need
to manually specify the internal unit system, it is useful for e.g.
testing against other simulation codes that use some other, fixed system
of units.
Non-linear parsing of parameter file
Parameters may be defined in terms of each other. Unlike with regular scripts, the definition order does not matter (i.e. you may reference a variable before it is defined). The magic that makes this work is cyclic execution of the parameter file, so that a variable that is needed on some line but only defined by some later line, is correctly set at the second execution. This non-linear variable dependence may continue arbitrarily, requiring many more execution passes.
In the following example, the initial scale factor value a_begin
is
included in the list of scale factor values at which to dump power
spectrum output, even though a_begin
is not defined before further down:
# Input/output
output_times = {'powerspec': [a_begin, 0.3, 1.0]}
# Cosmology
a_begin = 0.01
The path
, param
and jobid
objects
Available for use in parameter files is the convenient path
object, which
holds absolute paths to various useful directories and files. For example,
path.output_dir
evaluates to the output directory, which by default is
output
. If you prefer, you may instead use the dict
-like
syntax path['output_dir']
. The contents of the path
variable is
supplied by the .path
file.
Another convenience object available within parameter files is param
,
which evaluates to the name of the parameter file in which it is used. As an
example, consider this nice way of ensuring that the power spectrum output
gets dumped in a subdirectory of the output directory, labelled according to
the parameter file:
# Input/output
output_dirs = {'powerspec': f'{path.output_dir}/{param}'}
If used within the parameter file param/my_param
,
f'{path.output_dir}/{param}'
then evaluates to 'output/my_param'
.
The param
object is not just a plain str
though:
param.name
: Evaluates to the name of the parameter file (equivalent to to usingparam
on its own within astr
-context).param.path
: Evaluates to the absolute path of the parameter file.param.dir
: Evaluates to the absolute path of the directory containing the parameter file.
Lastly, the jobid
variable is also available within parameter files. This
is the unique integer ID labelling the current job. To e.g. direct power
spectrum output to a directory named after this ID, one can use
# Input/output
output_dirs = {'powerspec': f'{path.output_dir}/{jobid}'}
This way, previous outputs are not overwritten when running multiple simulations using the same parameter file.
Custom non-parameter variables
Using custom non-parameter variables in parameter files can help provide
better organisation. For example, you may want to define both the boxsize
and the resolution of the gravitational potential grid in terms of a common
variable:
# Non-parameter helper variable used to control the size of the simulation
_size = 256
# Numerics
boxsize = _size*Mpc
potential_options = 2*_size
Both the boxsize
and the potential grid size given by
potential_options
can now be simultaneously updated through the newly
introduced _size
variable, which itself is not a parameter. When defined
in a parameter file, CONCEPT treats any variable whose name does not
begin with an underscore ‘_
’ as a proper parameter; hence ‘_size
’ and
not ‘size
’.
Note
The potential_options
parameter is in fact much more involved than what
it would appear from the above example. The potential_options
is an
example of a “nested” parameter, with many “sub-parameters” definable
within it. For your convenience, CONCEPT allows for a plethora of
different non-complete specifications of such nested parameters (with
non-specified sub-parameters being auto-assigned), simplifying the writing
of parameter files. See the full specification
of the potential_options
parameter for details.
Inferred parameters
Some parameters are automatically defined based on the values set for other
parameters. These may then be used when defining further parameters.
Currently, the only two such inferred parameters are h
and Ων
:
h
is simply defined through \(h \equiv H_0/(100\, \text{km}\, \text{s}^{-1}\, \text{Mpc}^{-1})\), with the Hubble constant \(H_0\) being a normal parameter,H0
, defined as e.g.# Cosmology H0 = 67*km/(s*Mpc)
in which case
h
is set equal to0.67
. Havingh
available is useful if you want to use the common unit of \(\text{Mpc}/h\), e.g. when defining the box size:# Numerics boxsize = 256*Mpc/h
Ων
is the total density parameter \(\Omega_\nu\) for all massive neutrino species. It is set based on the massive neutrino parameters defined by theclass_params
parameter. As the computation of \(\Omega_\nu\) is non-trivial, this is nice to have available for simulations with massive neutrinos where the sum \(\Omega_{\text{cdm}} + \Omega_\nu\) is constrained. With e.g. \(\Omega_{\text{cdm}} + \Omega_\nu = 0.27\), one would set# Cosmology Ωcdm = 0.27 - Ων
in the parameter file. As
Ων
is non-trivial to compute from the user-defined parameters (class_params
), its value is printed at the beginning of the CONCEPT run (if running with massive neutrinos).
Caution
You must not yourself explicitly define any inferred parameters within parameter files.
Dynamic value insertion using ellipses
Several of the parameters are dict
s with which one often wants the
values to be identical for multiple keys. Instead of typing out the same
value multiple times, this may be copied over dynamically from the placement
of ellipses ‘...
’ like so:
# Input/output
output_dirs = {
'snapshot' : f'{path.output_dir}/{param}',
'powerspec': ...,
'bispec' : ...,
'render2D' : ...,
'render3D' : ...,
}
Here, the keys 'powerspec'
, 'bispec'
, 'render2D'
and
'render3D'
will all map to the same value as 'snapshot'
. More
generally, an ellipsis is replaced by the first non-ellipsis value encountered
when looking back up the key-value definitions, wrapping around if necessary.
Thus,
# Input/output
output_times = {
'snapshot' : ...,
'powerspec': 1,
'bispec' : ...,
'render2D' : ...,
'render3D' : [0.1, 0.3, 1],
}
results in 'snapshot'
being mapped to [0.1, 0.3, 1]
while 'bispec'
and 'render2D'
are being mapped to 1
.
Note
Another takeaway from the above example is the fact that CONCEPT
rarely is picky about the exact data types used when defining parameters.
We see that the keys of the output_times
dict
may be either single
numbers or list
s of numbers, which again may be either int
s or
float
s. Furhtermore, the container used did not have to be
a list
, but might instead have been e.g. a tuple
(0.1, 0.3, 1)
or a set
{0.1, 0.3, 1}
.
Checking parameter specifications
With all the possible magic surrounding parameter specifications, one might feel the need to explicitly check exactly how a given parameter ends up being defined. For this, you can do either of:
Run CONCEPT with a printout of specific parameters (here
parameter0
andparameter1
) as a substitute for the usual main entry point, e.g../concept --pure-python \ [-p param] [-c cmd-param] \ -m "from commons import *; \ print(f'{parameter0 = }'); \ print(f'{parameter1 = }'); \ "
Run CONCEPT in interactive mode like so:
./concept --pure-python -i -m "from commons import *;" \ [-p param] [-c cmd-param]
and then check the values of the parameters on the interactive Python prompt:
>>> parameter0 >>> parameter1 >>> exit()
Here brackets [...]
indicate optional command-line arguments, with
param
the parameter file and cmd-param
any
command-line parameters.
Remember that all dimensional parameters are expressed in accordance with the employed unit system.
Components and selections
In CONCEPT, the different species (e.g. matter, neutrinos) within a simulation are each represented by a component. Typically, the matter component consists of some number \(N\) of particles. All particles within a component are identical, so that they have the same mass, softening length, etc. Besides particle components, CONCEPT also implements fluid components. Conceptually, such components consist of a grid of cells, where again each cell share the same properties (e.g. equation of state). The type of a component — either ‘particles’ or ‘fluid’ — is called its representation.
Though all components must have an assigned species, a component is not identical to its species: A species is a particular (usually physical) substance inhabiting particular traits. A component is a given numerical implementation of such a species, with additional properties on top. One such property is a unique name, which however typically is just set equal to the name of the species. See the specification of initial conditions for how to separate the component name from its species.
Note
To get some practical experience with both particle and fluid components of various species, check out the tutorial, especially the large section going beyond matter-only simulations.
Several parameters either are or contain so-called component selections.
These are dict
s which assign some property (dict
value) to the
component(s) (dict
key).
An example of a component selection is the select_softening_length
parameter. Say we have two particle
components; one named 'spam'
of species 'baryon'
and one named
'ham'
of species 'cold dark matter'
. We can then assign separate
softening lengths to particles within each of these components like so:
select_softening_length = {
'spam': 25*kpc,
'ham' : 15*kpc,
}
The component selection system is however much more flexible. Beside names, components may also be referenced using their species:
select_softening_length = {
'baryon' : 25*kpc,
'cold dark matter': 15*kpc,
}
If we wish to employ the same softening length for all components, we can use the short-hand
select_softening_length = {
'all': 25*kpc,
}
Note
Often, properties like the softening length can be described as an expression common to all or multiple components, though the evaluated value may differ. Consider
select_softening_length = {
'all': '0.025*boxsize/cbrt(N)',
}
which sets the softening length to be \(2.5\,\%\) of the mean
inter-particle distance within each component separately. That is,
while boxsize
is a global constant, N
is understood to be an
attribute specific to each component, here its number of particles
\(N\).
We can also refer to components using their representation, like so:
select_softening_length = {
'particles': 25*kpc,
'fluid' : -1,
}
which again would assign \(25\,\text{kpc}\) as the softening length to
both 'spam'
and 'ham'
, assuming these are particle components. As
fluid components do not have an associated softening length, the value set for
'fluid'
does not matter for this particular component selection, even in
the case where fluid components are present within the simulation.
Component selections support the ellipsis syntax, as in
select_softening_length = {
'spam': 25*kpc,
'ham' : ...,
'all' : 15*kpc,
}
which assigns \(25\,\text{kpc}\) as the softening length to the
'spam'
and 'ham'
components and \(15\,\text{kpc}\) to
all others.
There is also a 'default'
key — which works similar to 'all'
—
but which should not be set by the user. Instead, this is set internally by
the code, intended to catch otherwise unset components within a selection.
Some component selection parameters further support component combinations.
An example is the powerspec_select
parameter.
The following specifies that we want power spectrum outputs for both
'spam'
and 'ham'
individually, as well as their combined (auto)
power spectrum:
powerspec_select = {
'spam' : True,
'ham' : True,
('spam', 'ham'): True,
}
The order in which the components are listed within a combination has
no significance, so ('spam', 'ham')
is equivalent to ('ham', 'spam')
.
Note
While it would be nice to use a set
{'spam', 'ham'}
to express this
lack of ordering, we cannot do so as set
s are not hashable and thus
cannot be used as keys in a dict
.
Any number of components may be paired together, not just two. Finally, all possible combinations of components may be specified as
powerspec_select = {
'all combinations': True,
}
Ensuring determinism
Reproducibility is a highly desirable feature of any complex software system. Running the same simulation twice, we of course expect very similar outcomes for the two runs. However, due to the non-associativity of floating-point arithmetic, two supposedly equivalent simulations generally do not produce exactly the same results (though any such differences ought to be much smaller than the inherent precision of the simulations).
Below we list the parameter settings needed in order to guarantee fully deterministic simulations, always yielding bitwise identical outputs (snapshots included) when run several times over. Note that this does come at a performance penalty.
random_seeds
: CONCEPT makes use of explicit (pseudo-)randomness for generating initial conditions, specifically the primordial noise. For the sake of reproducibility, this random noise is generated using particular random seeds, specified by therandom_seeds
parameter.For deterministic behaviour, you should then keep the values of these seeds fixed between simulations, e.g.
random_seeds = { 'primordial amplitudes': 1_000, # keep fixed between runs 'primordial phases' : 2_000, # keep fixed between runs }
shortrange_params
: By default, the decomposition of tiles into subtiles is dynamically updated throughout the simulation, based on CPU timing measurements (see the paper on ‘The cosmological simulation code CO𝘕CEPT 1.0’ for details). This naturally introduces indeterminism, as timing measurements are affected by whatever else is going on within the system, as well as the real-world physical circumstances regarding the hardware. The subtile decomposition is controlled by the'subtiling'
sub-parameter of theshortrange_params
parameter.For deterministic behaviour, you should set some static subtile decomposition, e.g.
shortrange_params = { 'subtiling': 2, }
particle_reordering
: The order in which particles of a component appear in memory ultimately determines the order in which direct particle-particle interactions — typically short-range gravity — are computed, corresponding to a specific ordering of the sum\[\boldsymbol{f}_i = -G m^2 \sum_{j} \frac{\boldsymbol{x}_i - \boldsymbol{x}_j}{|\boldsymbol{x}_i - \boldsymbol{x}_j|^3}\, ,\]for the force on particle \(i\) at \(\boldsymbol{x}_i\) due to all other particles \(j\) at \(\boldsymbol{x}_j\). As this sum consists of floating-point numbers it is not associative, and so the order matters for determinism. By default, the particles in memory are periodically re-sorted for performance reasons (see the
particle_reordering
parameter for details).For deterministic behaviour, you should restrict the in-memory reordering of particles to be done in a deterministic fashion,
particle_reordering = 'deterministic'
or disable the reordering entirely:
particle_reordering = False
fftw_wisdom_*
: The FFTW library used for the distributed FFTs is able to compute these in a multitude of different ways, with the results not being exactly identical to machine precision, introducing non-determinism into the simulations. Controlling the FFTW behaviour we have thefftw_wisdom_rigor
parameter, thefftw_wisdom_reuse
parameter and thefftw_wisdom_share
parameter.For deterministic behaviour, we can specify this set of parameters in a few different ways, depending on the circumstances.
If running CONCEPT locally or always on a single, specific node of a cluster, every simulation should make use of the same FFTW plans, stored as FFTW wisdom:
fftw_wisdom_reuse = True
Note that this is the default.
If running CONCEPT simulations on a cluster, possibly using a different set of nodes for different simulations, one should further allow the different nodes to share the same FFTW wisdom:
fftw_wisdom_reuse = True fftw_wisdom_share = True
Note that this is not the default.
With the above solutions for determinism within FFTW, the rigour level does not matter. An alternative to the above is to always stick with the lowest-level rigour, which estimates a good FFTW plan without running any performance tests, always resulting in the same plan for the same problem:
fftw_wisdom_rigor = 'estimate'
Note that this is not the default.
Besides the above parameter settings, the way in which CONCEPT is run may also break strict determinism:
Fixed set of parameters: On top of using specific parameter values as described above, the total set of parameters used should of course be exactly the same across the simulations, if exactly identical outputs are required.
Number of processes: The number of processes used to run the simulation directly dictates the domain decomposition and thus the order of various operations, again degrading the determinism due to the non-associativity of floating-point arithmetic. For deterministic results, always run with the same number of processes. How these processes are distributed across nodes does not matter, at least if the
fftw_wisdom_*
parameters are set according to the above.Different builds: Different systems (exact compiler version, operating system, etc.) may compile the CONCEPT source code into different machine code, which can lead to different results for otherwise equivalent simulations, performed on different systems. As usual, such differences should only be observed around the level of machine precision. For deterministic CONCEPT runs on a cluster, build the code once and make use of this build for all runs.
Different hardware: Running the same build of CONCEPT on different hardware can lead to slight differences in the results. If you work on a cluster with nodes of e.g. different CPU architectures, you should make sure to only and always use nodes of identical CPU architecture, if strict determinism is desirable.