Semi-Analytic Population Model
This page summarizes the semi-analytic SMBHB population model implemented in fastropop and shows how the main equations map onto the code.
The implementation is based on the semi-analytic SMBHB framework introduced in Sesana, Vecchio, and Colacino (2008).
Overview
The package models the nanohertz gravitational-wave background as a superposition of many distant supermassive black hole binaries (SMBHBs). Each binary is characterized by:
- chirp mass \(M\)
- redshift \(z\)
- observed frequency \(f\)
- sky position and orientation angles for skymaps and polarization realizations
The workflow in the code is:
- define a merger-rate density in mass and redshift
- convert that into a source-count density in mass, redshift, and frequency
- integrate to get ensemble quantities such as
h_c^2(f)and the expected source count - sample a Poisson realization of binaries from that distribution
- bin the sampled binaries into PTA-style strain spectra
Cosmology
The cosmology helpers are collected in src/fastropop/cosmology.py.
The model uses
and
In the code these correspond to:
EE(z)-> \(E(z)\)dtodz(z)-> \(dt_r / dz\)Dc_interp(z)-> comoving distance interpolationDL(z)-> luminosity distancedVcdz(z)-> \(dV_c / dz\)
Population Density in Mass and Redshift
The core semi-analytic model is the merger-rate density
The free parameters are
fastropop works internally with the density per unit mass rather than per unit log10(M), so the implemented quantity is
This is implemented in src/fastropop/semi_analytic.py:
SemiAnalyticPopulation.d2ndzdM(z, M)
The constructor parameters map directly as:
n0-> \(\dot n_0\)alphaM-> \(\alpha_{\mathcal M}\)Mstar-> \(\mathcal M_*\)betaz-> \(\beta_z\)z0-> \(z_0\)
Frequency Evolution
Assuming circular, GW-driven binaries, the rest-frame frequency evolves as
Since the code uses observed frequency f, the rest-frame frequency is
This is implemented as:
dlnfdtr(M, f, z)
where the (1+z) factor is already included internally.
Source Count Density in Mass, Redshift, and Frequency
The differential source count is built from the population density, the residence time in frequency, and the comoving volume element:
This is implemented in:
SemiAnalyticPopulation.d3ndzdMdlnf(M, f, z)
This is the key sampling density used to generate Monte Carlo realizations of binaries.
Ensemble Characteristic Strain
For the ensemble-averaged characteristic strain, the model uses
This is implemented by:
SemiAnalyticPopulation.hc2(ff)- internal helpers
_integrand,_integrand_log - NumPy/SciPy scalar integration helpers
_integrand_numpy,_integrand_log_numpy
The code currently uses SciPy for this integration path on purpose. The JAX-first version was slower here because adaptive scalar quadrature and JAX callbacks are a bad combination on CPU.
Expected Number of Binaries
The expected source count over a mass, redshift, and frequency range is
In the code this is:
SemiAnalyticPopulation.compute_Nbinaries(...)
The implementation integrates in log10(f) instead of ln(f), which introduces the expected Jacobian factor ln(10) in the integrand.
Strain of Individual Binaries
For a single binary, the polarization amplitudes can be written as
The code distinguishes between:
h(M, f, z)-> the un-averaged amplitude normalizationh_average(M, f, z)-> the sky-and-orientation averaged amplitude
The averaged amplitude used for stochastic background binning is
This is implemented by:
h_average(M, f, z)
The two are related by the sky-and-orientation average
Monte Carlo Realizations and Binning
The practical Monte Carlo recipe is:
- compute \(\langle N \rangle\)
- draw a Poisson realization \(N_s\)
- sample \(N_s\) binaries from \(d^3N/(dz \, d\mathcal M \, d\ln f)\)
- add the binary contributions within frequency bins
The binned characteristic strain estimator is
where the sum runs over sources in the i-th frequency bin.
This corresponds to:
generate_poisson_realization(...)-> Poisson draw of the source countsample_dist(...)-> draw masses, redshifts, and frequenciesbinning(...)-> compute the binned spectrum
More specifically, the code path in src/fastropop/semi_analytic.py is:
_prepare_sampling_distributions(...)builds separable sampling CDFssample_dist(...)draws \((M, z, \log_{10} f)\)binning_jitted(...)computes \(f \bar h^2\) and sums into PTA binsbinning(...)normalizes by the bin width and returns the spectrum array
Skymaps and Angular Variables
For spatially isotropic realizations, the manuscript samples:
- \(\phi \sim U[0, 2\pi)\)
- \(\cos\theta \sim U[-1,1]\)
- \(\cos\iota \sim U[-1,1]\)
- \(\psi \sim U[0,\pi)\)
These correspond to the random angular draws used in:
SemiAnalyticPopulation.generate_skymaps(...)
The skymap code combines sampled binary parameters with random sky position, inclination, polarization angle, and phase, then projects them into HEALPix pixels.
Summary of Equation-to-Code Correspondence
| Physics quantity | Equation | Code |
|---|---|---|
| \(E(z)\) | cosmology factor | EE |
| \(dt_r/dz\) | cosmic time-redshift relation | dtodz |
| \(dV_c/dz\) | comoving volume element | dVcdz |
| \(d^2 n / (dz \, dM)\) | semi-analytic merger density | SemiAnalyticPopulation.d2ndzdM |
| \(d\ln f_r / dt_r\) | GW-driven inspiral | dlnfdtr |
| \(d^3 N / (dz \, dM \, d\ln f)\) | source-count density | SemiAnalyticPopulation.d3ndzdMdlnf |
| \(h_c^2(f)\) | ensemble strain spectrum | SemiAnalyticPopulation.hc2 |
| \(\langle N \rangle\) | expected number of binaries | SemiAnalyticPopulation.compute_Nbinaries |
| \(\bar h(f)\) | averaged single-source strain | h_average |
| \(h_c^2(f_i)\) binned | Monte Carlo estimator | binning |
Related Notebook
For a worked tutorial focused on the model itself and its ensemble quantities, see:
examples/semi-analytic.ipynb