4. Mastering gravity
The fact that gravity is extremely important for the cosmic evolution has already been demonstrated. We would like our simulations to be able to compute gravity in a manner that is both accurate and efficient. In CONCEPT, the details of the gravitational computation is controlled by a large set of parameters, enabling us to tune this trade-off between accuracy and efficiency to our heart’s desire.
In order to learn how to control gravity within CONCEPT we shall run some sample simulations. The parameter specifications below are very similar to the ones encountered in the previous section.
# Non-parameter helper variable used to control the size of the simulation
_size = 64
# Input/output
initial_conditions = {
'species': 'matter',
'N' : _size**3,
}
output_dirs = {
'powerspec': f'{path.output_dir}/{param}',
}output_bases = { 'powerspec': f'powerspec_{_sim}',}output_times = {
'powerspec': 1,
}
# Numerics
boxsize = 256*Mpc
potential_options = _size
# Cosmology
H0 = 67*km/(s*Mpc)
Ωb = 0.049
Ωcdm = 0.27
a_begin = 0.02
# Physicsselect_forces = { 'matter': {'gravity': 'pm'},}
Save these parameters to e.g. param/tutorial-4
.
Now run a simulation using the tutorial parameter file by executing
./concept \
-p param/tutorial-4 \
-c "_sim = 'A'"
and optionally specifying whatever number of processes you’d like with the
-n
option.
The output_bases
parameter has been used to specify the base of filenames
for power spectrum output. This is set up to depend on a helper variable
_sim
, which we supply from the command-line. We can then run multiple
simulations without having their outputs overwrite each other, by supplying
different values for _sim
.
The result of the simulation will be a data file
output/tutorial-4/powerspec_A_a=1.00
of the matter power spectrum at
\(a = 1\), along with the corresponding plot
output/tutorial-4/powerspec_A_a=1.00.png
.
4.1. The PM method
A new parameter appearing in the tutorial parameter
file is select_forces
, which is set to map 'matter'
to the
gravitational interaction using the particle-mesh (PM) method. This method
works by solving the gravitational potential on a cubic grid, the size of
which is set through potential_options
. The potential used for our
first simulation, A, was then a cube of size
_size
\(\times\)_size
\(\times\)_size
(i.e. \(64\times 64\times 64\)), dividing the box into smaller cells.
The potential grid introduces a length scale below which gravity cannot be
resolved, namely the width of a grid cell, here boxsize/_size
. We may
expect to achieve a more accurate result by lowering this length scale, e.g.
by doubling the size (resolution) of the potential grid (in each direction).
To try this out, update potential_options
to
potential_options = 2*_size
in param/tutorial-4
and run a new simulation, this time using
./concept \
-p param/tutorial-4 \
-c "_sim = 'B'"
Visually comparing powerspec_A_a=1.00.png
to powerspec_B_a=1.00.png
,
we see that increasing the potential grid size leads to an increase in power,
as we would perhaps expect. To check whether doubling the grid size was
enough to achieve convergence, let’s further run a simulation C with triple
the grid size, i.e. switch to using
potential_options = 3*_size
in param/tutorial-4
and execute
./concept \
-p param/tutorial-4 \
-c "_sim = 'C'"
Properly comparing the three individual output plots is not so easy. To better compare the results, we should plot the different power spectra together in a single plot, using the information in the data files. You may do this using your favourite plotting tool, or — for your convenience — using the script below:
import glob, os, re
import numpy as np
import matplotlib; matplotlib.use('agg')
import matplotlib.pyplot as plt
# Read in data
this_dir = os.path.dirname(os.path.realpath(__file__))
k_sims, P_sims = {}, {}
for filename in sorted(glob.glob(f'{this_dir}/powerspec*'), key=os.path.getmtime):
match = re.search(r'powerspec_(.+)_', filename)
if not match or filename.endswith('.png'):
continue
sim = match.group(1)
k_sims[sim], P_sims[sim], P_lin_sim = np.loadtxt(
filename, usecols=(0, 2, 3), unpack=True,
)
if len(P_sims) == 1:
k = k_sims[sim]
P_lin = P_lin_sim
# Plot
fig, ax = plt.subplots()
linestyles = ['-', '--', ':', '-.']
for sim, P_sim in P_sims.items():
linestyle = linestyles[
sum(
np.allclose(line.get_ydata(), P_sim, 0.1)
for line in ax.lines
if len(line.get_ydata()) == len(P_sim)
)
%len(linestyles)
]
ax.loglog(k_sims[sim], P_sim, linestyle, label=f'simulation {sim}')
ylim = ax.get_ylim()
ax.loglog(k, P_lin, 'k--', label='linear', linewidth=1)
ax.set_xlim(k[0], k[-1])
ax.set_ylim(ylim)
ax.set_xlabel(r'$k\, [\mathrm{Mpc}^{-1}]$')
ax.set_ylabel(r'$P\, [\mathrm{Mpc}^3]$')
ax.legend()
fig.tight_layout()
fig.savefig(f'{this_dir}/plot.png', dpi=150)
Do not feel bad about using this plotting script — and others to come later
on in the tutorial — without studying it in any detail. To run the script,
save the above code to a file in the
output/tutorial-4
directory, say output/tutorial-4/plot.py
, then do
./concept -m output/tutorial-4/plot.py
Note
The -m
command-line option redirects concept
to run the specified
Python script, rather than launching a CONCEPT simulation. In this
case, this is almost equivalent to just running the script using Python
directly, but using ./concept -m
we are guaranteed that the environment
is set up correctly, according to the CONCEPT installation.
The script will produce the output output/tutorial-4/plot.png
.
Investigating this comparison plot, we see that in fact simulation
B — the one with the “in-between” potential grid size — has the
most power. It’s then unclear which of the three power spectra to trust,
if any.
Though inadequate for precision simulations, the PM method remains a valuable tool due to its unprecedented efficiency, as sometimes the benefits of rapid simulations outweigh the drawbacks from the loss of precision. Also, as we shall delve into later, PM gravity is as accurate as can be for fluid (i.e. non-particle) components, used to model species different from matter.
4.2. The P³M method
A different gravitational method is the particle-particle-particle-mesh (P³M) method. This method also makes use of a potential grid, but here the grid is used only to get the gravitational force between particles separated by a distance much greater than the potential cell size, minimizing discretisation errors. The remaining — so-called short-range — component of gravity between nearby particles is then computed separately, by directly pairing up particles and computing the force between them (no potential grid required).
To switch out PM for P³M, simply replace 'pm'
in the select_forces
parameter with 'p3m'
. Let’s run a simulation D using
select_forces = {
'matter': {'gravity': 'p3m'},
}
while also resetting potential_options
back to
potential_options = _size
You will notice that this time, the simulation takes much longer to complete. Studying the text printed to the screen, we see that the work within each time step is split into a short-range and a long-range part. From the computation times stated in parentheses to the right, it’s clear that the short-range part is by far the most time-consuming.
Once completed, add the P³M (D) result to the comparison plot by rerunning plot.py. You will see that P³M lies somewhere in-between B and C at intermediary scales, while having more power than any of the PM ones at the smallest scales.
As before, let’s now double the grid size, and perform a final simulation, E. You will find that this time, the P³M simulation isn’t nearly as slow, as the smaller potential cell size means that there is less work to be done for the short-range part.
Updating the comparison plot with the latest result, we see some long-awaited stability: The two P³M simulations give very similar results, and as such these are the ones in which we should put our trust.
As changing the size of the potential grid under P³M doesn’t much affect the
output, we are free to optimize it for performance. Generally, using a P³M
grid twice that of the “particle grid”
(potential_options
\(= 2 \sqrt[3]{N}\)) is recommended;
potential_options = 2*_size
Default parameters
You may wonder what gravitational method was used back for our previous
simulations, where we did not explicitly select a method through
select_forces
. Here it turns out that CONCEPT goes with P³M by
default.
For nested parameters such as select_forces
, it’s generally fine to not
write out everything in explicit detail, but instead rely on defaults. Thus,
specifying gravitational P³M may then be done through either of
select_forces = {
'matter': {'gravity', 'p3m'},
}
or
select_forces = {
'matter': 'gravity',
}
or by leaving out a specification for select_forces
entirely. See
here for more information about the select_forces
parameter.
Similarly, the potential_options
parameter is really a nested parameter by
which various details of the (PM and P³M)
potential can be controlled.
The PP method
Finally, let it be known that CONCEPT also supports the bare particle-particle (PP) method, which simply performs the naïve pairwise force computation between all particles, effectively computing “short”-range forces as in the P³M method, but now across the entire simulation box.
As expected, the PP method may be specified using
select_forces = {
'matter': {'gravity': 'pp'},
}
If you try this out, reduce _size
to at most 32, as running a PP
simulation with \(N = 64^3\) particles literately takes days. Though
slightly more accurate than the P³M method, the PP method should never be
used for running production simulations and is only included in CONCEPT
for use with internal testing. Running (well, starting) a small PP simulation
at least once will help any user of N-body codes appreciate the enormous
performance gains achieved by the more sophisticated methods.