Working with Simulation Directories

Analyzing simulation data is as important as running the simulation itself. In this notebook, we will see how we easily access all the data using the SimDir module.

(This notebook is meant to be converted in Sphinx documentation and not used directly.)

[1]:
from kuibit import simdir as sd

Loading simulation data is as easy as generating SimDir objects. Just provide the path, and kuibit will do the rest.

[2]:
sim = sd.SimDir("../../tests/tov")

We can get an overview of what is available by printing sim:

[3]:
print(sim)
Indexed 446 files and 5 subdirectories
Folder /home/runner/work/kuibit/kuibit/tests/tov
Available scalar timeseries:
['physical_time_per_hour', 'current_physical_time_per_hour', 'time_total', 'time_evolution', 'time_computing', 'time_communicating', 'time_io', 'evolution_steps_count', 'local_grid_points_per_second', 'total_grid_points_per_second', 'local_grid_point_updates_count', 'total_grid_point_updates_count', 'local_interior_points_per_second', 'total_interior_points_per_second', 'local_interior_point_updates_count', 'total_interior_point_updates_count', 'io_per_second', 'io_bytes_per_second', 'io_bytes_ascii_per_second', 'io_bytes_binary_per_second', 'io_count', 'io_bytes_count', 'io_bytes_ascii_count', 'io_bytes_binary_count', 'comm_per_second', 'comm_bytes_per_second', 'comm_count', 'comm_bytes_count', 'time_levels']

Available minimum timeseries:
['press', 'H', 'eps', 'vel[0]', 'vel[1]', 'vel[2]', 'alp', 'rho', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'M1', 'M2', 'M3']

Available maximum timeseries:
['vel[0]', 'vel[1]', 'vel[2]', 'alp', 'press', 'H', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'rho', 'eps', 'M1', 'M2', 'M3']

Available norm1 timeseries:
['gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'alp', 'H', 'press', 'M1', 'M2', 'M3', 'rho', 'eps', 'vel[0]', 'vel[1]', 'vel[2]']

Available norm2 timeseries:
['gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'vel[0]', 'vel[1]', 'vel[2]', 'rho', 'alp', 'press', 'eps', 'M1', 'M2', 'M3', 'H']

Available average timeseries:
['gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'alp', 'M1', 'M2', 'M3', 'press', 'H', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'eps', 'vel[0]', 'vel[1]', 'vel[2]', 'rho']

Variables available: dict_keys(['harmonic', 'psi4', 'phi2'])
For variable harmonic:

Avilable radii: [4.0, 8.0]

At radius 4.0, (l, m) available: dict_keys([(0, 0), (1, -1), (1, 0), (1, 1), (2, -1), (2, -2), (2, 0), (2, 1), (2, 2)])
At radius 8.0, (l, m) available: dict_keys([(0, 0), (1, -1), (1, 0), (1, 1), (2, -1), (2, -2), (2, 0), (2, 1), (2, 2)])

For variable psi4:

Avilable radii: [110.69, 48.95, 67.88, 77.93, 91.46, 60.13, 140.16, 53.96, 44.78, 191.0]

At radius 44.78, (l, m) available: dict_keys([(2, 0), (2, -2), (2, 2), (2, -1), (2, 1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 48.95, (l, m) available: dict_keys([(2, 1), (2, -1), (2, 0), (2, -2), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 53.96, (l, m) available: dict_keys([(2, -1), (2, -2), (2, 1), (2, 0), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 60.13, (l, m) available: dict_keys([(2, 0), (2, -2), (2, 1), (2, -1), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 67.88, (l, m) available: dict_keys([(2, -1), (2, 0), (2, 1), (2, 2), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 77.93, (l, m) available: dict_keys([(2, 2), (2, -2), (2, 1), (2, -1), (2, 0)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 91.46, (l, m) available: dict_keys([(2, -1), (2, 1), (2, 0), (2, 2), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 110.69, (l, m) available: dict_keys([(2, -2), (2, 1), (2, -1), (2, 2), (2, 0)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 140.16, (l, m) available: dict_keys([(2, -1), (2, 2), (2, 1), (2, -2), (2, 0)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 191.0, (l, m) available: dict_keys([(2, -1), (2, -2), (2, 2), (2, 1), (2, 0)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])

For variable phi2:

Avilable radii: [191.0, 48.95, 110.69, 140.16, 60.13, 77.93, 67.88, 91.46, 44.78, 53.96]

At radius 44.78, (l, m) available: dict_keys([(2, 0), (2, -1), (2, 2), (2, 1), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 48.95, (l, m) available: dict_keys([(2, 2), (2, 1), (2, -2), (2, 0), (2, -1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 53.96, (l, m) available: dict_keys([(2, -2), (2, 2), (2, 0), (2, -1), (2, 1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 60.13, (l, m) available: dict_keys([(2, 0), (2, 1), (2, -2), (2, 2), (2, -1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 67.88, (l, m) available: dict_keys([(2, 1), (2, -1), (2, 2), (2, 0), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 77.93, (l, m) available: dict_keys([(2, -1), (2, 0), (2, 1), (2, -2), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 91.46, (l, m) available: dict_keys([(2, -2), (2, -1), (2, 0), (2, 2), (2, 1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 110.69, (l, m) available: dict_keys([(2, 0), (2, -1), (2, 2), (2, 1), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 140.16, (l, m) available: dict_keys([(2, 2), (2, -2), (2, 1), (2, 0), (2, -1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 191.0, (l, m) available: dict_keys([(2, -2), (2, 1), (2, -1), (2, 0), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])

Available gravitational wave dataAvailable electromagnetic wave data
Available grid data of dimension 1D (x):
['kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'alp', 'rho', 'vel[0]', 'vel[1]', 'vel[2]', 'press', 'M1', 'M2', 'M3', 'H', 'eps']


Available grid data of dimension 1D (y):
['rho', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'vel[0]', 'vel[1]', 'vel[2]', 'H', 'eps', 'press', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'M1', 'M2', 'M3', 'alp']


Available grid data of dimension 1D (z):
['M1', 'M2', 'M3', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'rho', 'H', 'vel[0]', 'vel[1]', 'vel[2]', 'press', 'eps', 'alp', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz']


Available grid data of dimension 2D (xy):
['vel[0]', 'vel[2]', 'rho', 'vel[1]']


Available grid data of dimension 2D (xz):
['vel[2]', 'vel[0]', 'rho', 'vel[1]']


Available grid data of dimension 2D (yz):
['rho', 'vel[2]', 'vel[1]', 'vel[0]']


Available grid data of dimension 3D (xyz):
['vel[0]', 'vel[1]', 'vel[2]', 'mp_harmonic', 'rho']
No horizon found

This is a long output because we have a lot of available data! Everything that is printed is the output is easily accessible through SimDir. Moreover, the loading of data is lazy and the result is cached: computations are done only when needed, and all the subsequent calls will be immediate.

If you have thorns with custom output, you have to extend kuibit to analyze them. SimDir has multiple submodules to handle the different kind of data. Let us explore them.

SimDir and pickles

SimDir performs some operations only when they needed. This work has be done every time the Python interpreter is killed. In some cases, it is useful to save the progress to files, resulting in faster execution times. This can be done with pickles. The simplest way to do so is as follows.

[4]:
with sd.SimDir("../../tests/tov", pickle_file="/tmp/sim.pickle") as sim2:
    print(sim2)
Indexed 446 files and 5 subdirectories
Folder /home/runner/work/kuibit/kuibit/tests/tov
Available scalar timeseries:
['physical_time_per_hour', 'current_physical_time_per_hour', 'time_total', 'time_evolution', 'time_computing', 'time_communicating', 'time_io', 'evolution_steps_count', 'local_grid_points_per_second', 'total_grid_points_per_second', 'local_grid_point_updates_count', 'total_grid_point_updates_count', 'local_interior_points_per_second', 'total_interior_points_per_second', 'local_interior_point_updates_count', 'total_interior_point_updates_count', 'io_per_second', 'io_bytes_per_second', 'io_bytes_ascii_per_second', 'io_bytes_binary_per_second', 'io_count', 'io_bytes_count', 'io_bytes_ascii_count', 'io_bytes_binary_count', 'comm_per_second', 'comm_bytes_per_second', 'comm_count', 'comm_bytes_count', 'time_levels']

Available minimum timeseries:
['press', 'H', 'eps', 'vel[0]', 'vel[1]', 'vel[2]', 'alp', 'rho', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'M1', 'M2', 'M3']

Available maximum timeseries:
['vel[0]', 'vel[1]', 'vel[2]', 'alp', 'press', 'H', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'rho', 'eps', 'M1', 'M2', 'M3']

Available norm1 timeseries:
['gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'alp', 'H', 'press', 'M1', 'M2', 'M3', 'rho', 'eps', 'vel[0]', 'vel[1]', 'vel[2]']

Available norm2 timeseries:
['gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'vel[0]', 'vel[1]', 'vel[2]', 'rho', 'alp', 'press', 'eps', 'M1', 'M2', 'M3', 'H']

Available average timeseries:
['gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'alp', 'M1', 'M2', 'M3', 'press', 'H', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'eps', 'vel[0]', 'vel[1]', 'vel[2]', 'rho']

Variables available: dict_keys(['harmonic', 'psi4', 'phi2'])
For variable harmonic:

Avilable radii: [4.0, 8.0]

At radius 4.0, (l, m) available: dict_keys([(0, 0), (1, -1), (1, 0), (1, 1), (2, -1), (2, -2), (2, 0), (2, 1), (2, 2)])
At radius 8.0, (l, m) available: dict_keys([(0, 0), (1, -1), (1, 0), (1, 1), (2, -1), (2, -2), (2, 0), (2, 1), (2, 2)])

For variable psi4:

Avilable radii: [110.69, 48.95, 67.88, 77.93, 91.46, 60.13, 140.16, 53.96, 44.78, 191.0]

At radius 44.78, (l, m) available: dict_keys([(2, 0), (2, -2), (2, 2), (2, -1), (2, 1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 48.95, (l, m) available: dict_keys([(2, 1), (2, -1), (2, 0), (2, -2), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 53.96, (l, m) available: dict_keys([(2, -1), (2, -2), (2, 1), (2, 0), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 60.13, (l, m) available: dict_keys([(2, 0), (2, -2), (2, 1), (2, -1), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 67.88, (l, m) available: dict_keys([(2, -1), (2, 0), (2, 1), (2, 2), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 77.93, (l, m) available: dict_keys([(2, 2), (2, -2), (2, 1), (2, -1), (2, 0)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 91.46, (l, m) available: dict_keys([(2, -1), (2, 1), (2, 0), (2, 2), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 110.69, (l, m) available: dict_keys([(2, -2), (2, 1), (2, -1), (2, 2), (2, 0)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 140.16, (l, m) available: dict_keys([(2, -1), (2, 2), (2, 1), (2, -2), (2, 0)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 191.0, (l, m) available: dict_keys([(2, -1), (2, -2), (2, 2), (2, 1), (2, 0)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])

For variable phi2:

Avilable radii: [191.0, 48.95, 110.69, 140.16, 60.13, 77.93, 67.88, 91.46, 44.78, 53.96]

At radius 44.78, (l, m) available: dict_keys([(2, 0), (2, -1), (2, 2), (2, 1), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 48.95, (l, m) available: dict_keys([(2, 2), (2, 1), (2, -2), (2, 0), (2, -1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 53.96, (l, m) available: dict_keys([(2, -2), (2, 2), (2, 0), (2, -1), (2, 1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 60.13, (l, m) available: dict_keys([(2, 0), (2, 1), (2, -2), (2, 2), (2, -1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 67.88, (l, m) available: dict_keys([(2, 1), (2, -1), (2, 2), (2, 0), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 77.93, (l, m) available: dict_keys([(2, -1), (2, 0), (2, 1), (2, -2), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 91.46, (l, m) available: dict_keys([(2, -2), (2, -1), (2, 0), (2, 2), (2, 1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 110.69, (l, m) available: dict_keys([(2, 0), (2, -1), (2, 2), (2, 1), (2, -2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 140.16, (l, m) available: dict_keys([(2, 2), (2, -2), (2, 1), (2, 0), (2, -1)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])
At radius 191.0, (l, m) available: dict_keys([(2, -2), (2, 1), (2, -1), (2, 0), (2, 2)]) (missing: [(1, 0), (0, 0), (1, -1), (1, 1)])

Available gravitational wave dataAvailable electromagnetic wave data
Available grid data of dimension 1D (x):
['kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'alp', 'rho', 'vel[0]', 'vel[1]', 'vel[2]', 'press', 'M1', 'M2', 'M3', 'H', 'eps']


Available grid data of dimension 1D (y):
['rho', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'vel[0]', 'vel[1]', 'vel[2]', 'H', 'eps', 'press', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'M1', 'M2', 'M3', 'alp']


Available grid data of dimension 1D (z):
['M1', 'M2', 'M3', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'rho', 'H', 'vel[0]', 'vel[1]', 'vel[2]', 'press', 'eps', 'alp', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz']


Available grid data of dimension 2D (xy):
['vel[0]', 'vel[2]', 'rho', 'vel[1]']


Available grid data of dimension 2D (xz):
['vel[2]', 'vel[0]', 'rho', 'vel[1]']


Available grid data of dimension 2D (yz):
['rho', 'vel[2]', 'vel[1]', 'vel[0]']


Available grid data of dimension 3D (xyz):
['vel[0]', 'vel[1]', 'vel[2]', 'mp_harmonic', 'rho']
No horizon found

When this code is run, kuibit checks if a pickle file exists in /tmp/sim.pickle. If yes, it will load it, effectively “resuming a previous session”. At the end of the with block, the file is updated with the new work done. If the file did not exist initially, it will be created.

It is important to be careful when working with pickles as the risk of stale objects is high. If the data is changing (e.g., new restarts are added), it is likely that it will result in errors.

It is always possible to save the state of a SimDir using the save method and reads back the data with the load_SimDir function

[5]:
sim.save("/tmp/saved.pickle") # The filename can be anything
sim3 = sd.load_SimDir("/tmp/saved.pickle")

Cactus_scalars

The cactus_scalars module understands ASCII files as output by CarpetASCII. It supports all the reductions (maximum, minimum, …), 0d files, as well as scalar data. Using regular expression on the header of the file, cactus_scalars can process one variable per file and multiple variables per file. In SimDir, you can access scalars with the ts or timeseries attributes.

[6]:
timeseries = sim.ts
# equivalent: timeseries = sim.timeseries

If we printed timeseries, we would find part of the output from SimDir. This is because the printed output from SimDir is obtained printing all the different submodules. Another way to see the available variable for a secific reduction is with the keys method:

[7]:
timeseries.maximum.keys()
[7]:
dict_keys(['vel[0]', 'vel[1]', 'vel[2]', 'alp', 'press', 'H', 'kxx', 'kxy', 'kxz', 'kyy', 'kyz', 'kzz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'rho', 'eps', 'M1', 'M2', 'M3'])

There are equivalent three different way to access those variables as timeseries.

[8]:
rho_max1 = timeseries.maximum.fields.rho
rho_max2 = timeseries.maximum['rho']
rho_max3 = timeseries.maximum.get('rho')

These objects are timeseries, which are extremely convienent representation of data evolving in time. These series span multiple simulation restarts and have files in different folders: kuibit does all the hard work to find the files and combine them. Moreover, they timeseries have a lot of features, as you can see in the timeseries tutorial.