Creating your first population#
Welcome to the cogsworth tutorials! In this tutorial we will create a simple population of binaries.
Learning Goals#
By the end of this tutorial you should know:
What the
Populationclass can doHow to initialise a simple
PopulationHow to create a full population with basic settings using
create_population()
[15]:
import cogsworth
import gala.potential as gp
import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np
[3]:
# this all just makes plots look nice
%config InlineBackend.figure_format = 'retina'
plt.rc('font', family='serif')
plt.rcParams['text.usetex'] = False
fs = 24
# update various fontsizes to match
params = {'figure.figsize': (12, 8),
'legend.fontsize': fs,
'axes.labelsize': fs,
'xtick.labelsize': 0.9 * fs,
'ytick.labelsize': 0.9 * fs,
'axes.linewidth': 1.1,
'xtick.major.size': 7,
'xtick.minor.size': 4,
'ytick.major.size': 7,
'ytick.minor.size': 4}
plt.rcParams.update(params)
The Population Class#
The core functionality of cogsworth is to simulate populations of binaries in the context of the galaxy.
This is implemented primarily through the Population class, which will give you access
to the vast majority of cogsworth’s features.
Let’s initialise a simple population of binaries using the default input settings:
[5]:
p = cogsworth.pop.Population(n_binaries=100, processes=4,
final_kstar1=list(range(16)), final_kstar2=list(range(16)),
sfh_model=cogsworth.sfh.Wagg2022,
galactic_potential=gp.MilkyWayPotential(version='v2'),
v_dispersion=5 * u.km / u.s,
max_ev_time=12.0 * u.Gyr, timestep_size=1 * u.Myr,
BSE_settings={}, sampling_params={}, store_entire_orbits=True,
use_default_BSE_settings=True)
Basic input settings#
The Population class has a number of input settings that can be used to control the way
in which the population is created. Many of these directly affect the way in which a population is evolved or
the initial conditions are sampled and these are covered in later tutorials.
For now, we will focus on the basic settings:
n_binaries: By default,cogsworthsamples a population by targetting a number of binaries, this is that target valueprocesses: This is the number of CPUs to use for the population, which uses a multiprocessing pool for a variety of tasks. In general, the speed of the simulation scales with the number of processors.max_ev_time: How long to evolve a simulation for
The rest of the settings will be addressed in those other tutorials. Try running the next part with different values for these parameters and seeing what happens
Creating a population#
The manual way#
First let’s look a how you create a population manually, step-by-step (which can be useful for custom populations later on!).
[6]:
p = cogsworth.pop.Population(n_binaries=1000, processes=4, max_ev_time=12.0 * u.Gyr,
use_default_BSE_settings=True)
Initial stellar population#
Let’s start by sampling initial stellar population. This will create the _initial_binaries table as well as store values for the mass_singles, mass_binaries, n_singles_req, n_bin_req, which track the mass and number of single stars and binary stars necessary to sample this population.
Check out the sampling tutorial for more info and changing the sampling settings.
[7]:
# now let's get the initital stellar population
p.sample_initial_binaries()
p._initial_binaries
[7]:
| index | kstar_1 | kstar_2 | mass_1 | mass_2 | porb | ecc | metallicity | tphysf | mass0_1 | ... | tacc_1 | tacc_2 | epoch_1 | epoch_2 | tms_1 | tms_2 | bhspin_1 | bhspin_2 | tphys | binfrac | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0.0 | 0.0 | 0.157566 | 0.132221 | 197194.064203 | 0.622412 | 0.013086 | 5682.087153 | 0.157566 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 1 | 1 | 0.0 | 0.0 | 0.660025 | 0.220672 | 47.162822 | 0.027374 | 0.010722 | 6579.674348 | 0.660025 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 2 | 2 | 1.0 | 1.0 | 11.815256 | 8.041803 | 8.478898 | 0.019202 | 0.019260 | 6643.735592 | 11.815256 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 3 | 3 | 0.0 | 0.0 | 0.472437 | 0.202251 | 2110.567622 | 0.143982 | 0.013821 | 6660.107450 | 0.472437 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 4 | 4 | 1.0 | 1.0 | 1.405933 | 1.368043 | 36.503566 | 0.351799 | 0.011648 | 3456.999292 | 1.405933 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1303 | 1303 | 0.0 | 0.0 | 0.321245 | 0.315190 | 1.667328 | 0.097813 | 0.007657 | 10601.159078 | 0.321245 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 1304 | 1304 | 0.0 | 0.0 | 0.282806 | 0.087128 | 18.389173 | 0.522408 | 0.012857 | 8281.116937 | 0.282806 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 1305 | 1305 | 0.0 | 0.0 | 0.241308 | 0.154405 | 2.889133 | 0.587927 | 0.016882 | 8470.409857 | 0.241308 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 1306 | 1306 | 1.0 | 0.0 | 1.099550 | 0.618352 | 12160.670995 | 0.027828 | 0.008546 | 9869.178612 | 1.099550 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| 1307 | 1307 | 1.0 | 0.0 | 0.815015 | 0.493761 | 69.382317 | 0.654700 | 0.009505 | 7792.279021 | 0.815015 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
1308 rows × 39 columns
[8]:
fig, ax = plt.subplots()
ax.scatter(p._initial_binaries["mass_1"], p._initial_binaries["mass_2"])
ax.set(xscale="log", yscale="log",
xlabel=r"Initial primary mass $[\rm M_\odot]$", ylabel=r"Initial secondary mass $[\rm M_\odot]$")
plt.show()
Initial galaxy sampling#
Now let’s try sampling the initial galactic positions, kinematics and birth times. This will create the initial_galaxy attribute, which is a Galaxy that was instantiated based on your input choice of sfh_model when creating the Population.
Check out the star formation history tutorial for more info and changing initial galaxy sampling settings.
[9]:
# first let's sample the initial galaxy
p.sample_initial_galaxy()
# and check distributions look normal
plt.hist(p.initial_galaxy.rho.value, bins="fd");
plt.xlabel(r"Galactocentric $\rho \, [\rm kpc]$");
Stellar evolution#
Now we can use COSMIC to evolve the stellar population from birth until present day. This will create a series of output tables: initC (the initial conditions for each binary), bpp (the evolution table each binary), bcm (the conditioned timestep evolution table for each binary) and kick_info (the kicks experienced by each binary).
Check out this tutorial for more info and changing the population synthesis settings.
[10]:
p.perform_stellar_evolution()
p.bpp
[10]:
| tphys | mass_1 | mass_2 | kstar_1 | kstar_2 | sep | porb | ecc | RRLO_1 | RRLO_2 | ... | B_2 | bacc_1 | bacc_2 | tacc_1 | tacc_2 | epoch_1 | epoch_2 | bhspin_1 | bhspin_2 | bin_num | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.000000 | 0.157566 | 0.132221 | 0 | 0 | 9431.345261 | 197194.064203 | 0.622412 | 0.000138 | 0.000130 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 0 |
| 0 | 5682.087153 | 0.157566 | 0.132221 | 0 | 0 | 9431.345261 | 197194.064203 | 0.622412 | 0.000138 | 0.000130 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 0 |
| 1 | 0.000000 | 0.660025 | 0.220672 | 0 | 0 | 52.637351 | 47.162822 | 0.027374 | 0.025274 | 0.016495 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 1 |
| 1 | 6579.674348 | 0.660025 | 0.220672 | 0 | 0 | 52.637334 | 47.162798 | 0.027374 | 0.025789 | 0.016525 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 1 |
| 2 | 0.000000 | 11.815256 | 8.041803 | 1 | 1 | 47.367793 | 8.478898 | 0.019202 | 0.238627 | 0.226264 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 2 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1306 | 7792.922709 | 0.849999 | 0.619220 | 6 | 0 | 3106.837676 | 16557.958413 | 0.027791 | 0.152780 | 0.000553 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | -6207.630424 | 37.576069 | 0.0 | 0.0 | 1306 |
| 1306 | 7793.902178 | 0.460000 | 0.624964 | 11 | 0 | 3794.605436 | 26008.446229 | 0.027525 | 0.000011 | 0.000396 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 7793.902178 | 284.499601 | 0.0 | 0.0 | 1306 |
| 1306 | 9869.178612 | 0.460000 | 0.624964 | 11 | 0 | 5102.149166 | 40550.311918 | 0.027525 | 0.000008 | 0.000296 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 7793.902178 | 284.499601 | 0.0 | 0.0 | 1306 |
| 1307 | 0.000000 | 0.815015 | 0.493761 | 1 | 0 | 77.697338 | 69.382317 | 0.654700 | 0.065259 | 0.050086 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 1307 |
| 1307 | 7792.279021 | 0.815015 | 0.493761 | 1 | 0 | 77.695127 | 69.379355 | 0.654690 | 0.070742 | 0.050747 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 1307 |
4139 rows × 46 columns
[11]:
# print out some evolution information for the most massive binary
# we'll cover more on how this masking works in the next few tutorials
p.bpp.loc[p.initial_binaries[p.initial_binaries["mass_1"] == p.initial_binaries["mass_1"].max()]["bin_num"].iloc[0]]
[11]:
| tphys | mass_1 | mass_2 | kstar_1 | kstar_2 | sep | porb | ecc | RRLO_1 | RRLO_2 | ... | B_2 | bacc_1 | bacc_2 | tacc_1 | tacc_2 | epoch_1 | epoch_2 | bhspin_1 | bhspin_2 | bin_num | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 966 | 0.000000 | 21.290014 | 9.558130 | 1 | 1 | 6363.572493 | 10592.760098 | 0.330790 | 0.002544 | 0.002257 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | 0.000000 | 0.000000 | 0.0 | 0.0 | 966 |
| 966 | 9.339993 | 20.982598 | 9.556822 | 2 | 1 | 6427.900606 | 10808.003948 | 0.330790 | 0.006110 | 0.002608 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | -0.094812 | -0.001113 | 0.0 | 0.0 | 966 |
| 966 | 9.355676 | 20.979662 | 9.556820 | 4 | 1 | 6428.518958 | 10810.083668 | 0.330790 | 0.023743 | 0.002608 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | -0.094812 | -0.001117 | 0.0 | 0.0 | 966 |
| 966 | 10.295175 | 18.631822 | 9.643188 | 5 | 1 | 5828.143893 | 9697.642571 | 0.069340 | 0.660799 | 0.002052 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | -0.094812 | 0.148776 | 0.0 | 0.0 | 966 |
| 966 | 10.312262 | 18.476582 | 9.652698 | 5 | 1 | 5751.590690 | 9531.797508 | 0.049206 | 0.660799 | 0.002052 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | -0.094812 | 0.165256 | 0.0 | 0.0 | 966 |
| 966 | 10.312262 | 8.032066 | 9.652698 | 14 | 1 | -1.000000 | -1.000000 | -1.000000 | 0.000000 | -2.000000 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | 10.312262 | 0.165256 | 0.0 | 0.0 | 966 |
| 966 | 27.860784 | 8.032066 | 9.634363 | 14 | 2 | -1.000000 | -1.000000 | -1.000000 | 0.000100 | 0.000100 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | 10.312262 | 0.089712 | 0.0 | 0.0 | 966 |
| 966 | 27.942244 | 8.032066 | 9.632528 | 14 | 3 | -1.000000 | -1.000000 | -1.000000 | 0.000100 | 0.000100 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | 10.312262 | 0.080960 | 0.0 | 0.0 | 966 |
| 966 | 27.947231 | 8.032066 | 9.631883 | 14 | 4 | -1.000000 | -1.000000 | -1.000000 | 0.000100 | 0.000100 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | 10.312262 | 0.080960 | 0.0 | 0.0 | 966 |
| 966 | 30.845812 | 8.032066 | 9.406922 | 14 | 5 | -1.000000 | -1.000000 | -1.000000 | 0.000100 | 0.000100 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | 10.312262 | 0.080960 | 0.0 | 0.0 | 966 |
| 966 | 30.951651 | 8.032066 | 9.298136 | 14 | 5 | -1.000000 | -1.000000 | -1.000000 | 0.000100 | 0.000100 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | 10.312262 | 0.080960 | 0.0 | 0.0 | 966 |
| 966 | 30.951651 | 8.032066 | 1.277584 | 14 | 13 | -1.000000 | -1.000000 | -1.000000 | 0.000100 | 0.000100 | ... | 0.000000e+00 | 0.0 | 0.0 | 0.0 | 0.0 | 10.312262 | 30.951651 | 0.0 | 0.0 | 966 |
| 966 | 11947.433362 | 8.032066 | 1.277584 | 14 | 13 | -1.000000 | -1.000000 | -1.000000 | 0.000100 | 0.000100 | ... | 4.737887e+10 | 0.0 | 0.0 | 0.0 | 0.0 | 10.312262 | 30.951651 | 0.0 | 0.0 | 966 |
13 rows × 46 columns
Galactic evolution#
Finally, let’s bring Gala into the mix and evolve this population of binaries through a galactic potential. This will create orbits for each binary in orbits that can be accessed easily as primary_orbits and secondary_orbits.
Check out the potentials tutorial for more info and changing the galactic evolution settings (in particular the potential).
[12]:
p.perform_galactic_evolution()
p.orbits
Integrating orbits: 100%|██████████| 1314/1314 [00:02<00:00, 556.00it/s]
[12]:
array([<Orbit cartesian, dim=3, shape=(7771,)>,
<Orbit cartesian, dim=3, shape=(762,)>,
<Orbit cartesian, dim=3, shape=(6339,)>, ...,
<Orbit cartesian, dim=3, shape=(10750,)>,
<Orbit cartesian, dim=3, shape=(11112,)>,
<Orbit cartesian, dim=3, shape=(10325,)>],
shape=(1314,), dtype=object)
[16]:
fig, ax = plt.subplots()
p.orbits[np.random.randint(0, len(p.orbits))].cylindrical.plot(["rho", "z"], axes=[ax]);
The easy way#
Now that was useful to understand what’s happening behind the scenes, but for many cases you just want to efficiently create a population. This can be done all at once with the create_population().
[19]:
p.create_population()
Run for 1000 binaries
Sampled 1348 binaries
[1e-02s] Sample initial binaries
[0.6s] Evolve binaries (run COSMIC)
Integrating orbits: 100%|██████████| 1350/1350 [00:02<00:00, 584.73it/s]
[4.4s] Integrate galactic orbits (run gala)
Overall: 5.0s
Check out the next tutorial to learn more about interpretting the outputs of these simulations.
Wrap-up#
And that’s all for this first tutorial of the basic series, nice job! We covered a lot here and much of the functionality I used will get explained in more detail in the later tutorials. Press your right arrow key for the next installment or head over to the main tutorials page for more options.
Note
This tutorial was generated from a Jupyter notebook that can be found here.