Your First Binary Simulations with POSYDON 🌠

Tutorial goal:

In this tutorial, we will run a small population of 10 binaries locally, and explore how to manipulate the output data from your population.

New concepts:

  • Population ini file

If you haven’t done so yet, export the path POSYDON environment variables. Set these parameters in your .bash_profile or .zshrc if you use POSYDON regularly.

[ ]:
%env PATH_TO_POSYDON=/YOUR/POSYDON/PATH/
%env PATH_TO_POSYDON_DATA=/YOUR/POSYDON_DATA/PATH/

Creating the Initialisation File


To run population synthesis with POSYDON, a population_params.ini file is required. This file described how the stellar population is created and what prescriptions and parameters are implemented in specific steps.

POSYDON comes with a default population_params_default.ini file found at PATH_TO_POSYDON/posydon/popsyn or have a look here.

The file is split in three main parts. You can find more details about their properties by clicking on their links. 1. `SimulationProperties <>`__: - these describe the properties and parameters of different steps in the evolution of a binary systems. 2. `BinaryPopulation <>`__: - parameters of the initial sampling of the binary population, such as initial mass function, period distribution, and metallicity. - Also contains parameters on how the population is ran, such as how many binaries are kept in memory. 3. `SavingOutput <>`__ - Describes the data from the binary and each individual star to the output files.

We will copy the default population run parameter file to the current folder.

[ ]:
import os
import shutil
from posydon.config import PATH_TO_POSYDON

path_to_params = os.path.join(PATH_TO_POSYDON, "posydon/popsyn/population_params_default.ini")
shutil.copyfile(path_to_params, './population_params.ini')

Reprocessed POSYDON v1 data

If you’re using the reprocessed POSYDON v1 dataset, you will only have solar metallicity available! This means you will only be able to run populations with metallicity = [1]!

You should be able to follow along with this tutorial and can follow along with the “One metallicity notebook”, but the multi-metallicity component is not yet available.

Creating and running a binary population

The copied population_params.ini contains the parameters to run 10 binaries at a metallicity of \(Z=1 Z_\odot\).

If you open the file and scroll down to the BinaryPopulation section, you will see how they’re defined:

metallicity = [1] # [2., 1., 0.45, 0.2, 0.1, 0.01, 0.001, 0.0001]
# In units of solar metallicity
...
number_of_binaries = 10
# int

If you like to run a small population in a notebook, you can use the PopulationRunner to do this. If you want to run a specific binary instead, have a look at the `Binary Tutorial <>`__.

The PopulationRunner class takes the .ini file and sets-up a population run.

This will create `BinaryPopulations <>`__ for each metallicity defined in the .ini file. In this case, we can check and see that a single BinaryPopulation is created and contains 10 binaries.

[ ]:
from posydon.popsyn.synthetic_population import PopulationRunner
poprun = PopulationRunner('./population_params.ini', verbose=True)
[ ]:
print('Number of binary populations:',len(poprun.binary_populations))
print('Metallicity:', poprun.binary_populations[0].metallicity)
print('Number of binaries:', poprun.binary_populations[0].number_of_binaries)

For this tutorial, we set verbose=True, which shows you the progress of the population run. This overwrites the population verbose set inside the population_params.ini file.

Now we are ready to evolve the binary population. This should take about 30 seconds, but depends on your machine.

[ ]:
poprun.evolve()

Inspecting the population: Population class

When you ran the population, you might have seen that a temporary folder with the name 1e+00_Zsun_batches was created while the binaries were evolved. This is a temporary folder in which populations are temporarly saved. After the binary evolution has finished, the binaries in the folder are moved to a single file named 1e+00_Zsun_popululation.h5. This is done automatically, when you run a population using the PopulationRunner class.

The created file contains 3 main components:

  1. history: the evolution of an individual binary in a pandas DataFrame

  2. oneline: a single line to describe the initial and final conditions and some one-of parameters, such as the metallicity.

  3. mass_per_metallicity: some metadata on the population, such as the total simulated mass, the actual underlying mass of the population, and the number of binaries in the file.

The Population provides an interface with these components in the file, such that you’re able to share the populations runs and can work with large population that do not fit in memory.

Older Population Files

If you’re using older population files, you can make them compatible with the Population class by calling Population(pop_file, metallicity,ini_file), where the metallicity is in solar units. You will only need to do this once; afterwards you can initialise the class like normal.

[ ]:
from posydon.popsyn.synthetic_population import Population
[ ]:
pop = Population('1e+00_Zsun_population.h5', verbose=True)

The pop.mas_per_met shows some basic information about the population you’ve just created.

  1. The index (metallicity) is the metallicity of your population in solar units.

  2. simulated mass is the total ZAMS mass that has been evolved in the population.

  3. underlying mass is the actual mass of the population if one integrates the IMF and period distribution fully.

  4. number_of_systems shows the 10 systems in the file.

[ ]:
pop.mass_per_metallicity

There are some additional metadata properties available, such as: - metallicities: the metallicity in absolute metallicity - solar_metallicities: the metallicities in the file in solar metallicity - number_of_systems: the total number of systems in the Population file - indices: the indices of the binaries in the file - columns: the columns available in the history and oneline dataframes

[ ]:
# you can also access the total number of systems in the file with
print(pop.number_of_systems)

Population.history

[ ]:
pop.history.lengths

pop.history loads in the full history of all the binaries into memory. You can access individual or a selections of the population using several methods:

  1. pop.history[5]

  2. pop.history[[0,4]]

  3. pop.history[‘time]

  4. pop.history.select()

The select function is the most powerfull way to access the binaries, because it allows you to perform selections based on the specific columns available in the history dataframe. For example, below we can select on state == 'RLO1', which gives us all the rows with RLO1 occuring.

The available identifiers are limited to string columns (state, event, step_names, S1_state, S2_state), index, and columns names.

[ ]:
# select only binary_index 5
pop.history[5]
[ ]:
# select binary 0 and 4
pop.history[[0,4]]
[ ]:
pop.history['time'].head()

You can also check what columns are available in the history file:

[ ]:
pop.history.columns
[ ]:
# using the select function
pop.history.select(where='index == 9')
[ ]:
# selecting all RLO1 states and only time and state columns
pop.history.select(where='state == RLO2', columns=['time', 'state'])
[ ]:
# selecting rows 10 to 16
pop.history.select(start=10, stop=16)

You might have notices while using the above functions that not all the binaries will have the same length in the history. You can access these with pop.history_lengths. This information is also stored in the population file.

[ ]:
pop.history_lengths

Population.oneline

Population.oneline provides a similar interface to accessing the DataFrame in the population file as Population.history, with similar functionality being available.

The select function only has acces to: - index - column names - string columns: state_i, state_f, event_i, event_f, step_names_i, step_names_f, S1_state_i, S1_state_f, S2_state_i, S2_state_f, S1_SN_type, S2_SN_type, interp_class_HMS_HMS, interp_class_CO_HeMS, interp_class_CO_HMS_RLO, interp_class_CO_HeMS_RLO, mt_history_HMS_HMS, mt_history_CO_HeMS, mt_history_CO_HMS_RLO, mt_history_CO_HeMS_RLO

[ ]:
pop.oneline[5]
pop.oneline[[0,4]]
pop.oneline.select(where='index == 9')
pop.oneline.select(where='index == [0,9]')
[ ]:
pop.oneline.columns

Population.formation_channels

You might be interested in figuring out what sort of formation pathways/channels a binary has followed through its evolution.

This is not a standard output of the population synthesis, but you can include it into the population file by calculating it. If you would like more detail on the initial mass transfer, you can set mt_history=True.

This will write the formation channels to the Population file, which can be accessed by Population.formation_channels.

[ ]:
pop.calculate_formation_channels(mt_history=True)
[ ]:
# the formation channels are loaded in with pop.formation_channels
pop.formation_channels

Next time you open this population file, the formation_channels will be available without having to be recalculated.

[ ]:
pop = Population('1e+00_Zsun_population.h5')
pop.formation_channels

Selecting a sub-population

You might just want a small sub-selection of the full population, especially if you’re working with large population and multi-metallicity runs.

The Population.export_selection() function will export just the indices of the binaries you’re interested in into a new file. The simulated and underlying mass will remain the same, since they are dependent on the population run.

If we select just 2 binaries and export them, we create a new population of just the binaries you’re interested in. In the `BBH analysis <>`__ and `GRB analysis <>`__ tutorials, we show how to perform a selection with multiple criteria and metallicities.

[ ]:
indices = [0,9]
pop.export_selection(indices, 'selected.h5')
[ ]:
selected = Population('selected.h5')
selected.mass_per_metallicity

If you would like to know the simulated mass of just your population, you can calulate this using the online ZAMS values.

[ ]:
import numpy as np

print('selected simulated mass: ', np.sum(selected.oneline[['S1_mass_i', 'S2_mass_i']].to_numpy()))

By default this export-selection will not overwrite nor append if the output file is already present. You have to explicitly state what you would like to append to or overwrite the population file.

With the append=True you are able to combine multiple stellar populations into a single file. This is especially useful when creating multi-metallicity populations.

[ ]:
# this will overwrite the existing file
pop.export_selection(indices, 'selected.h5', overwrite=True)

selected = Population('selected.h5')
selected.mass_per_metallicity

[ ]:
# This will add to the file and add the extra simulated mass
pop.export_selection(indices, 'selected.h5', append=True)
selected = Population('selected.h5')
selected.mass_per_metallicity

Feel free to explore the small binary population you’ve just created!

If you want to learn more about population synthesis and how to build more complex models it is advised to continue with the remaining tutorials and consult the POSYDON documentation.

Local MPI runs

To speed up population synthesis runs, you can run on a computing cluster, as described in HPC Facilities, or you can distribute the population synthesis across multiple cores on your local machine using MPI.

To enable local MPI runs, go into the population_params.ini and change use_MPI to True.

It’s important to note that you cannot run have this option enabled for cluster runs!

We create a binary population simulation script to run the population:

[ ]:
%%writefile script.py
from posydon.popsyn.synthetic_population import PopulationRunner

if __name__ == "__main__":
    synth_pop = PopulationRunner("./population_params.ini")
    synth_pop.evolve()

This script can be initiated using a local where NR_processors is the number of processors you would like to us.

[ ]:
mpiexec -n ${NR_processors} python script.py

This will create a folder for each metallicity in the population and store output of the parallel runs in it.

You will have to concatenate these runs manually into a single population file per metallicity, which can be achieved using the following code:

[ ]:
from posydon.popsyn.synthetic_population import PopulationRunner

synth_pop = PopulationRunner("./population_params.ini")
for pop in synth_pop.binary_populations:
    synth_pop.merge_parallel_runs(pop)