A component of the data release for "The Population of Merging Compact Binaries Inferred Using Gravitational Waves Through GWTC-3"
Authors: Bruce Edelman, Amanda Farah, and Jacob Golomb on behalf of the LIGO Scientific Collaboration, Virgo Collaboration and KAGRA Collaboration
This software is provided under license: Creative Commons Attribution 4.0 International (https://creativecommons.org/licenses/by/4.0/legalcode)
This notebook is intented to show readers how to load in the data associated with some of the gwpopulation-based models used in "The population of merging compact binaries inferred using gravitational waves through GWTC-3" (herein "the companion paper").
Here, we will show you how to load in hyperposterior samples, plot probability distributions as a function of source parameters, and make less-polished versions of the plots in the paper.
The models highlighed in this particular tutorial are:
These are all included in or built off of the open source python package, gwpopulation, which includes several other parameterized distributions as well, many of which are highlighted in "Population Properties of Compact Objects from the Second LIGO-Virgo Gravitational-Wave Transient Catalog."
Note that other parameterized (MultiSource and NS mass models) and non-parametric (Binned Gaussian Process, Flexible Mixtures) models not based on gwpopulation exist and are used in the companion paper. There are separate tutorials for those.
This notebook will not show you how to run the population analysis. If you are intersted in this, please look at the documention for gwpopulation: https://colmtalbot.github.io/gwpopulation/
from gwpopulation.models.redshift import PowerLawRedshift
from bilby.core.result import read_in_result
from scipy.interpolate import interp1d
import numpy as np
import deepdish as dd
import matplotlib.pyplot as plt
We will begin by reading in the results of the population inference on our default event list for the BBH population. In this example, we will read in the results for the Powerlaw + Peak model. These results are stored as bilby result files in this data repository. A description of the parameters used in this model can be found in Appendix B1 of the companion paper.
PP_path = '../analyses/PowerLawPeak/o1o2o3_mass_c_iid_mag_iid_tilt_powerlaw_redshift_result.json'
PP_result = read_in_result(PP_path)
There is a lot of information in the Result object. Lets take a look at some useful attributes.
print("run label: ", PP_result.label)
print("log10 BF: ", PP_result.log_10_bayes_factor) # note that this will not always be the same BF that is quoted in the text because some corrections are necessary to compare these between models
print("\n", "hyper-priors:")
PP_result.priors
run label: o1o2o3_mass_c_iid_mag_iid_tilt_powerlaw_redshift log10 BF: 38.46431085780439 hyper-priors:
{'alpha': Uniform(minimum=-4, maximum=12, name='alpha', latex_label='$\\alpha$', unit=None, boundary=None),
'beta': Uniform(minimum=-2, maximum=7, name='beta', latex_label='$\\beta_{q}$', unit=None, boundary=None),
'mmax': Uniform(minimum=30, maximum=100, name='mmax', latex_label='$m_{\\max}$', unit=None, boundary=None),
'mmin': Uniform(minimum=2, maximum=10, name='mmin', latex_label='$m_{\\min}$', unit=None, boundary=None),
'lam': Uniform(minimum=0, maximum=1, name='lambda', latex_label='$\\lambda_{m}$', unit=None, boundary=None),
'mpp': Uniform(minimum=20, maximum=50, name='mpp', latex_label='$\\mu_{m}$', unit=None, boundary=None),
'sigpp': Uniform(minimum=1, maximum=10, name='sigpp', latex_label='$\\sigma_{m}$', unit=None, boundary=None),
'delta_m': Uniform(minimum=0, maximum=10, name='delta_m', latex_label='$\\delta_{m}$', unit=None, boundary='reflective'),
'amax': DeltaFunction(peak=1.0, name=None, latex_label=None, unit=None),
'mu_chi': Uniform(minimum=0, maximum=1, name='mu_chi', latex_label='$\\mu_{\\chi}$', unit=None, boundary=None),
'sigma_chi': Uniform(minimum=0.005, maximum=0.25, name='sigma_chi', latex_label='$\\sigma^{2}_{\\chi}$', unit=None, boundary=None),
'alpha_chi': Constraint(minimum=1, maximum=100000.0, name=None, latex_label=None, unit=None),
'alpha_chi_1': Constraint(minimum=1, maximum=100000.0, name=None, latex_label=None, unit=None),
'alpha_chi_2': Constraint(minimum=1, maximum=100000.0, name=None, latex_label=None, unit=None),
'beta_chi': Constraint(minimum=1, maximum=100000.0, name=None, latex_label=None, unit=None),
'beta_chi_1': Constraint(minimum=1, maximum=100000.0, name=None, latex_label=None, unit=None),
'beta_chi_2': Constraint(minimum=1, maximum=100000.0, name=None, latex_label=None, unit=None),
'xi_spin': Uniform(minimum=0, maximum=1, name='xi_spin', latex_label='$\\zeta$', unit=None, boundary=None),
'sigma_spin': Uniform(minimum=0.1, maximum=4.0, name='sigma_t', latex_label='$\\sigma_t$', unit=None, boundary=None),
'lamb': Uniform(minimum=-10, maximum=10, name='lamb', latex_label='$\\lambda_z$', unit=None, boundary=None)}
What you are probably most intersted in are the hyperposterior samples, which are saved as a pandas dataframe.
PP_hyperposterior_samples = PP_result.posterior.copy() # making a copy is best practice here so you don't accidentally modify things in-place
PP_hyperposterior_samples
| alpha | beta | mmax | mmin | lam | mpp | sigpp | delta_m | mu_chi | sigma_chi | ... | lamb | amax | log_likelihood | log_prior | selection | pdet_n_effective | surveyed_hypervolume | log_10_rate | rate | min_event_n_effective | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2.116671 | 0.645405 | 84.257180 | 3.858558 | 0.075468 | 30.270897 | 8.346910 | 3.511792 | 0.262951 | 0.033104 | ... | -1.007767 | 1.0 | 86.740514 | -20.167742 | 0.006983 | 6630.971573 | 300.455132 | 1.475926 | 29.917560 | 205.407820 |
| 1 | 2.357356 | 0.504086 | 78.325899 | 5.301450 | 0.123904 | 38.688342 | 4.317169 | 2.576325 | 0.247116 | 0.028849 | ... | -2.460752 | 1.0 | 88.476899 | -20.167742 | 0.016273 | 3772.629171 | 107.519753 | 1.521225 | 33.206611 | 84.680631 |
| 2 | 2.763145 | -1.038759 | 92.335648 | 4.512187 | 0.136947 | 26.322204 | 9.283129 | 3.411572 | 0.201743 | 0.024835 | ... | 4.922529 | 1.0 | 89.160301 | -20.167742 | 0.000089 | 4852.535111 | 69321.160923 | 1.075940 | 11.910773 | 107.429424 |
| 3 | 2.975530 | 1.204292 | 81.253295 | 5.193624 | 0.095203 | 40.661528 | 6.232922 | 4.752846 | 0.223713 | 0.024432 | ... | -1.466976 | 1.0 | 89.577067 | -20.167742 | 0.010964 | 4340.422763 | 213.598090 | 1.485615 | 30.592485 | 89.268950 |
| 4 | 2.336330 | 0.888358 | 83.282458 | 3.218458 | 0.150673 | 27.806907 | 6.098497 | 5.846578 | 0.240640 | 0.019683 | ... | 1.572624 | 1.0 | 89.923126 | -20.167742 | 0.001794 | 5813.075241 | 2629.581289 | 1.171681 | 14.848441 | 80.848769 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 11464 | 3.417291 | 0.301697 | 85.168951 | 4.798439 | 0.035291 | 33.889739 | 2.350728 | 5.505413 | 0.230583 | 0.020566 | ... | 3.264948 | 1.0 | 108.115543 | -20.167742 | 0.000339 | 2972.807715 | 13097.278496 | 1.214318 | 16.380142 | 71.881363 |
| 11465 | 3.328885 | 1.642387 | 79.491399 | 4.931802 | 0.034236 | 34.720684 | 2.136944 | 5.327197 | 0.254585 | 0.032604 | ... | 2.109967 | 1.0 | 108.120336 | -20.167742 | 0.000872 | 3152.917529 | 4322.664396 | 1.369043 | 23.390707 | 72.814645 |
| 11466 | 3.328885 | 1.642387 | 79.491399 | 4.931802 | 0.034236 | 34.720684 | 2.136944 | 5.327197 | 0.254585 | 0.032604 | ... | 2.109967 | 1.0 | 108.120336 | -20.167742 | 0.000872 | 3152.917529 | 4322.664396 | 1.288162 | 19.416100 | 72.814645 |
| 11467 | 3.548725 | 0.760204 | 83.140164 | 4.815572 | 0.019037 | 34.467261 | 1.866748 | 5.453627 | 0.235676 | 0.024758 | ... | 3.431551 | 1.0 | 108.206956 | -20.167742 | 0.000256 | 3214.364467 | 15429.210409 | 1.259669 | 18.183135 | 76.580270 |
| 11468 | 3.548725 | 0.760204 | 83.140164 | 4.815572 | 0.019037 | 34.467261 | 1.866748 | 5.453627 | 0.235676 | 0.024758 | ... | 3.431551 | 1.0 | 108.206956 | -20.167742 | 0.000256 | 3214.364467 | 15429.210409 | 1.184209 | 15.283016 | 76.580270 |
11469 rows × 22 columns
We can pull 1D marginal distributions from this by picking out a specific column. As an example, lets try the powerlaw slope on m1
plt.hist(PP_hyperposterior_samples.alpha, bins=50, density=True, histtype='step')
plt.xlabel(r"$\alpha$")
plt.show()
We can plot all of these samples with their many correlations using a corner plot:
c = PP_result.plot_corner(save=False)
More documentation on how to make these corner plots to fit your needs can be found here (see also: plot_multiple).
While informative, these corner plots are not very intuitive to discern useful information. Each sample in the above distribution corresponds to a unique mass, spin, and redshift population distribution. A more informative demonstration of the significance of this posterior distribution is to plot the inferred distributions in parameter space; that is, given the above draws from the posterior distribution, what do the resulting inferred mass, spin, and redshift distributions look like?
We have stored these distributions for the some draws from the hyperposterior in the following hdf5 files in this repository:
Mass: o1o2o3_mass_c_iid_mag_iid_tilt_powerlaw_redshift_mass_data.h5
Spin Magnitude: o1o2o3_mass_c_iid_mag_iid_tilt_powerlaw_redshift_orientation_data.h5
Redshift: o1o2o3_mass_c_iid_mag_iid_tilt_powerlaw_redshift_redshift_data.h5
Begin by reading in the mass distribution-make sure the path below points to the above files in the /analyses/PowerLawPeak subdirectory of the data release!
mass_PP_path = '../analyses/PowerLawPeak/o1o2o3_mass_c_iid_mag_iid_tilt_powerlaw_redshift_mass_data.h5'
with open(mass_PP_path, 'r') as _data:
_data = dd.io.load(mass_PP_path)
print(_data.keys())
print(_data['lines'].keys())
dict_keys(['lines', 'ppd']) dict_keys(['mass_1', 'mass_ratio'])
The HDF5 file for the mass distribution contains both the individual merger rate vs mass lines determined by the population parameters in the posterior distribution, as well as the resulting Posterior Predictive Distribution (PPD), the "average" rate distribution as determined by the draws of the population parameters.
Recall that we have defined the Powerlaw + Peak mass model in terms of two separable one-dimensional distributions: a probability distribution of m1 (primary mass) and distribution for q=m2/m1 (mass ratio). Thus, to get the inferred distribution for one variable we must marginalize over the other. We will do exactly this here; plot the inferred primary mass distribution and mass ratio distribution by marginalizing over the other in turn. Note that what we have stored in these files and what we will be plotting is the differential rate, which is closely related to the probability density in terms of the mass, spin, or redshift.
If you would like the following figures saved, set "outdir" to your desired output directory and set save=True when we call the below function. Let's begin by defining the mass distribution plotting function.
def mass_spectrum_plot(filenames, limits, labels):
"""
Generates a plot of the PPD and X% credible region for the mass distribution,
where X=limits[1]-limits[0]
"""
mass_1 = np.linspace(2, 100, 1000)
mass_ratio = np.linspace(0.1, 1, 500)
mass_1_grid, mass_ratio_grid = np.meshgrid(mass_1, mass_ratio)
fig, axs = plt.subplots(2, 1, figsize=(12, 8))
# load in the traces.
# Each entry in lines is p(m1 | Lambda_i) or p(q | Lambda_i)
# where Lambda_i is a single draw from the hyperposterior
# The ppd is a 2D object defined in m1 and q
for filename, label in zip(filenames, labels):
with open(filename, 'r') as _data:
_data = dd.io.load(filename)
lines = _data["lines"]
ppd = _data["ppd"]
# marginalize over q to get the ppd in terms of m1 only
mass_1_ppd = np.trapz(ppd, mass_ratio, axis=0)
# marginalize over m1 to get the ppd in terms of q only
mass_ratio_ppd = np.trapz(ppd, mass_1, axis=-1)
# plot the PPD as a solid line
axs[0].semilogy(mass_1, mass_1_ppd, label=label)
# plot the CIs as a filled interval
axs[0].fill_between(
mass_1,
np.percentile(lines["mass_1"], limits[0], axis=0),
np.percentile(lines["mass_1"], limits[1], axis=0),
alpha=0.5,
)
axs[1].semilogy(mass_ratio, mass_ratio_ppd)
axs[1].fill_between(
mass_ratio,
np.percentile(lines["mass_ratio"], limits[0], axis=0),
np.percentile(lines["mass_ratio"], limits[1], axis=0),
alpha=0.5,
)
axs[0].set_xlim(2, 100)
axs[0].set_ylim(1e-3, 10)
axs[0].set_xlabel("$m_{1}$ [$M_{\\odot}$]")
axs[0].legend(loc="best")
ylabel = "$\\frac{d\\mathcal{R}}{dm_{1}}$ [Gpc$^{-3}$yr$^{-1}M_{\\odot}^{-1}$]"
axs[0].set_ylabel(ylabel)
axs[1].set_xlim(0.1, 1)
axs[1].set_ylim(1e-2, 200)
axs[1].set_xlabel("$q$")
ylabel = "$\\frac{d\\mathcal{R}}{dq}$ [Gpc$^{-3}$yr$^{-1}$]"
axs[1].set_ylabel(ylabel)
plt.tight_layout()
return fig
Now let's actually call the above plotting script to generate the inferred mass distribution for the primary mass and mass ratio. Note we set the credibility interval we wish to plot via the "limits" argument in the plotting function. Let's plot the 90% credible region.
fig = mass_spectrum_plot([mass_PP_path], limits=[5,95], labels=['PowerLaw+Peak'])
Looks good! Compare this with Figure 10 in the companion paper. Now we can make a similar plot of the spin magnitude distribution (in terms of $a_1$ and $a_2$, the dimensionless spin magnitude of the primary and secondary black hole in a binary, respectively).
Again, we begin by noticing we have the set of "lines" and the PPD data, as we had with the mass distribution HDF5 file.
spin_PP_path = '../analyses/PowerLawPeak/o1o2o3_mass_c_iid_mag_iid_tilt_powerlaw_redshift_magnitude_data.h5'
with open(spin_PP_path, 'r') as _data:
_data = dd.io.load(spin_PP_path)
print(_data.keys())
print(_data['lines'].keys())
dict_keys(['lines', 'ppd']) dict_keys(['a_1', 'a_2'])
def spin_magnitude_spectrum_plot(filename, limits):
"""
Generates a plot of the PPD and X% credible region for the component spin magnitude distributions,
where X=limits[1]-limits[0]
"""
mags = np.linspace(0, 1, 1000)
a_1, a_2 = np.meshgrid(mags, mags)
fig, axs = plt.subplots(1, 2, figsize=(12, 4))
with open(filename, 'r') as _data:
_data = dd.io.load(filename)
ppd = _data["ppd"]
lines = dict()
for key in _data["lines"].keys():
lines[key] = _data["lines"][key][()]
a_1_ppd = np.trapz(ppd, mags, axis=0)
a_2_ppd = np.trapz(ppd, mags, axis=1)
label = r"Spin Magnitude ($a_1$)"
axs[0].plot(mags, a_1_ppd, label=label)
axs[0].fill_between(
mags,
np.percentile(lines["a_1"], limits[0], axis=0),
np.percentile(lines["a_1"], limits[1], axis=0),
alpha=0.5,
)
label = r"Spin Magnitude ($a_1$)"
axs[1].plot(mags, a_2_ppd, label=label)
axs[1].fill_between(
mags,
np.percentile(lines["a_2"], limits[0], axis=0),
np.percentile(lines["a_2"], limits[1], axis=0),
alpha=0.5,
)
for ii in [1, 2]:
axs[ii - 1].set_xlim(0, 1)
axs[ii - 1].set_ylim(0)
axs[ii - 1].set_xlabel(f"$a_{ii}$")
ylabel = f"$\\frac{{dN}}{{da_{ii}}}$"
axs[ii - 1].set_ylabel(ylabel)
axs[ii - 1].legend(loc="best")
plt.tight_layout()
return fig
Again, we will plot the 90% credible region of the inferred distribution:
fig = spin_magnitude_spectrum_plot(spin_PP_path, limits=[5,95])
Finally, we can plot the inferred redshift distribution: the probability of a merger occuring at a particular redshift (or distance) based on our inferred evolution of merger rate with redshift. This will follow the above process closely. We begin by loading the path:
redshift_PP_path = '../analyses/PowerLawPeak/o1o2o3_mass_c_iid_mag_iid_tilt_powerlaw_redshift_redshift_data.h5'
with open(redshift_PP_path, 'r') as _data:
_data = dd.io.load(redshift_PP_path)
print(_data.keys())
print(_data['lines'].keys())
dict_keys(['lines', 'ppd']) dict_keys(['redshift'])
def redshift_spectrum_plot(filename, limits):
"""
Generates a plot of the PPD and X% credible region for the merger rate as a function of redshift,
where X=limits[1]-limits[0]
"""
fig, ax = plt.subplots(1, 1, figsize=(6, 4))
model = PowerLawRedshift(z_max=1.9)
redshifts = model.zs_
with open(filename, 'r') as _data:
_data = dd.io.load(filename)
lines = _data["lines"]
ppd = _data["ppd"]
label = 'Redshift Distribution'
ax.plot(redshifts, ppd, label=label)
ax.fill_between(
redshifts,
np.percentile(lines["redshift"], limits[0], axis=0),
np.percentile(lines["redshift"], limits[1], axis=0),
alpha=0.5,
)
ylabel = "$\\mathcal{R}(z)$ [Gpc$^{-3}$yr$^{-1}$]"
ax.set_xlim(0, 1)
ax.set_yscale("log")
ax.set_xlabel("$z$")
ax.set_ylabel(ylabel)
ax.legend(loc="best")
plt.tight_layout()
return fig
redshift_fig = redshift_spectrum_plot(redshift_PP_path, limits=[5,95])
Compare this with Figure 13 in the companion paper. Note we see an interesting result here: The rate is decidely increasing with z. This means that we infer that the rate of BBH mergers is larger the farther away we look. If we plot a straight line over this plot, corresponding to a merger rate that does not evolve with redshift, we notice it lies well outside the inferred 90% credible region
with open(redshift_PP_path, 'r') as _data:
_data = dd.io.load(redshift_PP_path)
local_rate = _data["ppd"][0]
plt.figure()
redshift_spectrum_plot(redshift_PP_path, limits=[5,95])
plt.axhline(y= local_rate, color = 'r', linestyle=':', label='Non-Evolving Merger Rate')
plt.legend()
plt.show()
<Figure size 432x288 with 0 Axes>
We can now apply these skills to the other gwpopulation-based models. Lets try Power Law + Dip + Break
Outputs from this model are stored in analyses/PowerlawDipBreak/. Power Law + Peak was labeled as mass_c, and Power Law + Dip + Break runs are labeled as mass_g and mass_h. g is for the independent pairing model (Eq. 10), whereas h is for the power-law-in-mass-ratio pairing model (Eq. 11).
We will start by looking at the full set of hyperparameters for the independent pairing model.
PDB_path = '../analyses/PowerlawDipBreak/O3all_mass_g_iid_mag_iid_tilt_powerlaw_redshift_result.json'
PDB_result = read_in_result(PDB_path)
c = PDB_result.plot_corner(save=False)
Note that for the sake of comparison with other joint analyses, runs of this model set the spin distribution to be uniform in magnitude and isotropic in orientation, so the spin parameters are not fit and therefore show uninformative posteriors. This is expected, but ugly to look at. We can ignore them by specifying only the mass distribution parameters.
c = PDB_result.plot_corner(parameters=['NSmin','NSmax','BHmin','BHmax','A','alpha_1','alpha_2','n3'], save=False)
Now for the mass spectra.
PDB is parameterized in terms of primary and secondary mass (m1 and m2) rather than primary mass and mass ratio. Therefore, the plotting script will be a bit different, so we will write a new one.
Also, the contents of the h5 files containing all of the traces are a little different for PDB.
mass_PDB_path = '../analyses/PowerlawDipBreak/O1O2O3all_mass_g_iid_mag_iid_tilt_powerlaw_redshift_mass_data.h5'
with open(mass_PDB_path, 'r') as _data:
_data = dd.io.load(mass_PDB_path)
print(_data.keys())
print(_data['lines'].keys())
dict_keys(['lines', 'ppd']) dict_keys(['mass', 'p_mass_1', 'p_mass_2', 'pdf', 'rate_mass_1', 'rate_vs_mass'])
So we see that we have a few options. We can plot the mass distribution as a function of m1 (p_mass_1), the mass distribution as a function of m2 (p_mass_2), the mass distribution as a function of generic component mass (pdf), or the differential merger rate as a function of either m1 or generic component mass.
Here, 'mass' is the 1D grid of masses on which the other entries are evaluated. mass is NOT p(m1), unlike for Powerlaw + Peak and other models parameterized by m1 and q.
def mass_spectrum_plot_m1m2(filenames, limits, labels, key='rate_vs_mass'):
"""
Generates a plot of the PPD and X% credible region for the component mass distribution for PDB model parameterized in m1/m2
where X=limits[1]-limits[0]
"""
fig = plt.figure()
for filename, label in zip(filenames, labels):
# load in the traces
with open(filename, 'r') as _data:
_data = dd.io.load(filename)
lines = _data["lines"]
ppd = _data["ppd"]
# plot the CIs
# here, lines['mass'] is the 1D grid of masses on which the other entries are evaluated
plt.fill_between(lines['mass'].T[0],
np.percentile(lines[key], limits[0], axis=0),
np.percentile(lines[key], limits[1], axis=0),
alpha=0.5,
)
# The PPD is the mean value of the probability distribution at each m
plt.semilogy(lines['mass'], lines[key].mean(axis=0),
linewidth=3, label=label
)
# We mostly use this model to look at the transition
# between NS and BH masses, so lets zoom in there
plt.xlim(1,10)
plt.ylim(10**-1.4, 10**3.6)
plt.yscale('log')
# label axes
plt.xlabel("$m$ [$M_{\\odot}$]")
# set y label depending on what we are plotting.
ylabel_dict = dict(
rate_vs_mass="$\mathrm{d}\mathcal{R}/\mathrm{d}m\ [\mathrm{Gpc}^{-3}\ \mathrm{ yr}^{-1}\ M_{\\odot}^{-1}]$",
rate_mass_1="$\mathrm{d}\mathcal{R}/\mathrm{d}m_1\ [\mathrm{Gpc}^{-3}\ \mathrm{ yr}^{-1}\ M_{\\odot}^{-1}]$",
p_mass_1="$p(m_1) [M_{\\odot}^{-1}]$",
p_mass_2="$p(m_2) [M_{\\odot}^{-1}]$",
pdf="$p(m) [M_{\\odot}^{-1}]$",
)
plt.ylabel(ylabel_dict[key])
plt.legend()
return fig
fig = mass_spectrum_plot_m1m2([mass_PDB_path], limits=[5,95], labels=['Power Law + Dip + Break'], key='rate_vs_mass')
Compare this with Figure 5.
If we also want to get the locations of the gap edges (or any other feature, for that matter), we can pull those from the hyperposterior.
M_gap_low = PDB_result.posterior.copy().NSmax
M_gap_high = PDB_result.posterior.copy().BHmin
As an example, let's slap the medians on the above plot. You could also take whichever percentiles you like, or a maximum posterior value. It is up to you.
axs = fig.gca()
axs.axvline(M_gap_low.median(), color='k')
axs.axvline(M_gap_high.median(), color='k')
fig
Now let's look at one of the other, more flexible, gwpopulation-based models used to analyze the BBH mass spectrum, the Power Law + Spline (PS) model. A description of the model and parameters used in these analyses can be found in Appendix B1 of the companion paper.
Outputs from this model are stored in analyses/PowerlawSpline/. Power Law + Spline was labeled as mass_m, for the particular choice of 20 knots used in this analysis.
Lets start by loading in the result file the same as the previous models.
PS_path = '../analyses/PowerLawSpline/o1o2o3/spline_20n_mass_m_iid_mag_iid_tilt_powerlaw_redshift_result.json'
PS_result = read_in_result(PS_path)
Now lets plot the (LARGE) corner plot. The PS mass model has 18 parameters that control the knot heights for the 18 interior knots (f1-f18). This one takes a while because there are so many parameters...
c = PS_result.plot_corner(save=False)
Again let's load in the mass_data.h5 file to plot the mass spectrum. This should be identical in format to the mass_PP_path file used above.
mass_PS_path = '../analyses/PowerLawSpline/o1o2o3/spline_20n_mass_m_iid_mag_iid_tilt_powerlaw_redshift_mass_data.h5'
with open(mass_PS_path, 'r') as _data:
_data = dd.io.load(mass_PS_path)
print(_data.keys())
print(_data['lines'].keys())
dict_keys(['lines', 'ppd']) dict_keys(['mass_1', 'mass_ratio'])
Now we can use the mass_spectrum_plot from above to overlay both the PS and PP mass distributions on top of each other, to get a good comparison of where they disagree.
fig = mass_spectrum_plot([mass_PP_path, mass_PS_path], limits=[5,95], labels=['Power Law + Peak', 'Power Law + Spline'])
Now lets look at specifically what the spline perutbrations is doing to the underlying powerlaw in our PS model. There is a perturbation applied from 2-100 solar masses with cubic splines, lets generate a plot similar to Figure 12 in the companion paper.
def plot_powerlaw_spline_perturbations(resultf, nsamps=1000):
"""
Generates a plot of the median and 90% credible region for cubic spline perturbation function applied to an underlying
powerlaw in the PS model.
"""
# Load in the bilby result object
result = read_in_result(resultf)
mgrid = np.linspace(2, 100, 1000)
fig = plt.figure(figsize=(9,6))
ax = plt.gca()
ax.set_rasterization_zorder(1)
# Loop over nsamps random posterior samples
splines = []
for i in range(nsamps):
idx = np.random.choice(np.arange(len(result.posterior)))
# Pull out knot locations/heights (mi's/fi's)
m_splines = [result.posterior[f"m{j}"][idx] for j in range(20)]
f_splines = [result.posterior[f"f{j}"][idx] for j in range(20)]
sel = (mgrid >= m_splines[0]) & (mgrid <= m_splines[-1])
# Do interpolation
spline = interp1d(m_splines, f_splines, kind="cubic")
plt.semilogx(
mgrid[sel], spline(mgrid[sel]), color='grey',lw=0.5,alpha=0.125,zorder=0
)
splines.append(spline(mgrid[sel]))
splines = np.array(splines)
# plot 90% CI
med = np.median(splines, axis=0)
low, high = np.quantile(splines, (0.05, 0.95), axis=0)
plt.plot(mgrid[sel], low, color="k")
plt.plot(mgrid[sel], high, color="k")
# plot vertical lines where the knot locations are
xs = [result.posterior[f"m{ii}"][0] for ii in range(20)]
lowy = np.interp(xs, mgrid[sel], low)
highy = np.interp(xs, mgrid[sel], high)
plt.vlines(xs, lowy, highy, color="k")
plt.xlim(2, 100)
ax = plt.gca()
plt.ylim(-3.5, 3.5)
ax.axhline(-1.6, color="tab:blue", ls="--", lw=2)
ax.axhline(1.6, color="tab:blue", ls="--", lw=2)
plt.ylabel(r"$f(m_1)$")
plt.xlabel(r"$m_1$")
plt.tight_layout()
return fig
fig = plot_powerlaw_spline_perturbations(PS_path)