# Source code for pycbc.pnutils

# Copyright (C) 2012  Alex Nitz
#
#
# This program is free software; you can redistribute it and/or modify it
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

#
# =============================================================================
#
#                                   Preamble
#
# =============================================================================
#
"""This module contains convenience pN functions. This includes calculating conversions
between quantities.
"""
import logging
import numpy

import lal
from scipy.optimize import bisect, brentq, minimize

from pycbc import conversions, libutils

logger = logging.getLogger('pycbc.pnutils')

lalsim = libutils.import_optional('lalsimulation')

[docs]def nearest_larger_binary_number(input_len):
""" Return the nearest binary number larger than input_len.
"""
return int(2**numpy.ceil(numpy.log2(input_len)))

[docs]def chirp_distance(dist, mchirp, ref_mass=1.4):
return conversions.chirp_distance(dist, mchirp, ref_mass=ref_mass)

[docs]def mass1_mass2_to_mtotal_eta(mass1, mass2):
m_total = conversions.mtotal_from_mass1_mass2(mass1, mass2)
eta = conversions.eta_from_mass1_mass2(mass1, mass2)
return m_total,eta

[docs]def mtotal_eta_to_mass1_mass2(m_total, eta):
mass1 = conversions.mass1_from_mtotal_eta(m_total, eta)
mass2 = conversions.mass2_from_mtotal_eta(m_total, eta)
return mass1,mass2

[docs]def mass1_mass2_to_mchirp_eta(mass1, mass2):
m_chirp = conversions.mchirp_from_mass1_mass2(mass1, mass2)
eta = conversions.eta_from_mass1_mass2(mass1, mass2)
return m_chirp,eta

[docs]def mchirp_eta_to_mass1_mass2(m_chirp, eta):
mtotal = conversions.mtotal_from_mchirp_eta(m_chirp, eta)
mass1 = conversions.mass1_from_mtotal_eta(mtotal, eta)
mass2 = conversions.mass2_from_mtotal_eta(mtotal, eta)
return mass1, mass2

[docs]def mchirp_mass1_to_mass2(mchirp, mass1):
"""
This function takes a value of mchirp and one component mass and returns
the second component mass. As this is a cubic equation this requires
finding the roots and returning the one that is real.
Basically it can be shown that:

m2^3 - a(m2 + m1) = 0

where

a = Mc^5 / m1^3

this has 3 solutions but only one will be real.
"""
return conversions.mass2_from_mchirp_mass1(mchirp, mass1)

[docs]def eta_mass1_to_mass2(eta, mass1, return_mass_heavier=False, force_real=True):
"""
This function takes values for eta and one component mass and returns the
second component mass. Similar to mchirp_mass1_to_mass2 this requires
finding the roots of a quadratic equation. Basically:

eta m2^2 + (2 eta - 1)m1 m2 + eta m1^2 = 0

This has two solutions which correspond to mass1 being the heavier mass
or it being the lighter mass. By default the value corresponding to
mass1 > mass2 is returned. Use the return_mass_heavier kwarg to invert this
behaviour.
"""
return conversions.mass_from_knownmass_eta(mass1, eta,
known_is_secondary=return_mass_heavier, force_real=force_real)

[docs]def mchirp_q_to_mass1_mass2(mchirp, q):
""" This function takes a value of mchirp and the mass ratio
mass1/mass2 and returns the two component masses.

The map from q to eta is

eta = (mass1*mass2)/(mass1+mass2)**2 = (q)/(1+q)**2

Then we can map from (mchirp,eta) to (mass1,mass2).
"""
eta = conversions.eta_from_q(q)
mass1 = conversions.mass1_from_mchirp_eta(mchirp, eta)
mass2 = conversions.mass2_from_mchirp_eta(mchirp, eta)
return mass1, mass2

[docs]def A0(f_lower):
"""used in calculating chirp times: see Cokelaer, arxiv.org:0706.4437
appendix 1, also lalinspiral/python/sbank/tau0tau3.py
"""
return conversions._a0(f_lower)

[docs]def A3(f_lower):
"""another parameter used for chirp times"""
return conversions._a3(f_lower)

[docs]def mass1_mass2_to_tau0_tau3(mass1, mass2, f_lower):
tau0 = conversions.tau0_from_mass1_mass2(mass1, mass2, f_lower)
tau3 = conversions.tau3_from_mass1_mass2(mass1, mass2, f_lower)
return tau0,tau3

[docs]def tau0_tau3_to_mtotal_eta(tau0, tau3, f_lower):
mtotal = conversions.mtotal_from_tau0_tau3(tau0, tau3, f_lower)
eta = conversions.eta_from_tau0_tau3(tau0, tau3, f_lower)
return mtotal, eta

[docs]def tau0_tau3_to_mass1_mass2(tau0, tau3, f_lower):
m_total,eta = tau0_tau3_to_mtotal_eta(tau0, tau3, f_lower)
return mtotal_eta_to_mass1_mass2(m_total, eta)

[docs]def mass1_mass2_spin1z_spin2z_to_beta_sigma_gamma(mass1, mass2,
spin1z, spin2z):
_, eta = mass1_mass2_to_mtotal_eta(mass1, mass2)
# get_beta_sigma_from_aligned_spins() takes
# the spin of the heaviest body first
heavy_spin = numpy.where(mass2 <= mass1, spin1z, spin2z)
light_spin = numpy.where(mass2 > mass1, spin1z, spin2z)
beta, sigma, gamma = get_beta_sigma_from_aligned_spins(
eta, heavy_spin, light_spin)
return beta, sigma, gamma

[docs]def get_beta_sigma_from_aligned_spins(eta, spin1z, spin2z):
"""
Calculate the various PN spin combinations from the masses and spins.
See <http://arxiv.org/pdf/0810.5336v3.pdf>.

Parameters
-----------
eta : float or numpy.array
Symmetric mass ratio of the input system(s)
spin1z : float or numpy.array
Spin(s) parallel to the orbit of the heaviest body(ies)
spin2z : float or numpy.array
Spin(s) parallel to the orbit of the smallest body(ies)

Returns
--------
beta : float or numpy.array
The 1.5PN spin combination
sigma : float or numpy.array
The 2PN spin combination
gamma : float or numpy.array
The 2.5PN spin combination
chis : float or numpy.array
(spin1z + spin2z) / 2.
"""
chiS = 0.5 * (spin1z + spin2z)
chiA = 0.5 * (spin1z - spin2z)
delta = (1 - 4 * eta) ** 0.5
spinspin = spin1z * spin2z
beta = (113. / 12. - 19. / 3. * eta) * chiS
beta += 113. / 12. * delta * chiA
sigma = eta / 48. * (474 * spinspin)
sigma += (1 - 2 * eta) * (81. / 16. * (chiS * chiS + chiA * chiA))
sigma += delta * (81. / 8. * (chiS * chiA))
gamma = (732985. / 2268. - 24260. / 81. * eta - \
340. / 9. * eta * eta) * chiS
gamma += (732985. / 2268. + 140. / 9. * eta) * delta * chiA
return beta, sigma, gamma

[docs]def solar_mass_to_kg(solar_masses):
return solar_masses * lal.MSUN_SI

[docs]def parsecs_to_meters(distance):
return distance * lal.PC_SI

[docs]def megaparsecs_to_meters(distance):
return parsecs_to_meters(distance) * 1e6

[docs]def velocity_to_frequency(v, M):
return conversions.velocity_to_frequency(v, M)

[docs]def frequency_to_velocity(f, M):
return conversions.frequency_to_velocity(f, M)

[docs]def f_SchwarzISCO(M):
"""
Innermost stable circular orbit (ISCO) for a test particle
orbiting a Schwarzschild black hole

Parameters
----------
M : float or numpy.array
Total mass in solar mass units

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
return conversions.f_schwarzchild_isco(M)

[docs]def f_BKLISCO(m1, m2):
"""
Mass ratio dependent ISCO derived from estimates of the final spin
of a merged black hole in a paper by Buonanno, Kidder, Lehner

Parameters
----------
m1 : float or numpy.array
First component mass in solar mass units
m2 : float or numpy.array
Second component mass in solar mass units

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
# q is defined to be in [0,1] for this formula
q = numpy.minimum(m1/m2, m2/m1)
return f_SchwarzISCO(m1+m2) * ( 1 + 2.8*q - 2.6*q*q + 0.8*q*q*q )

[docs]def f_LightRing(M):
"""
Gravitational wave frequency corresponding to the light-ring orbit,
equal to 1/(3**(3/2) pi M) : see InspiralBankGeneration.c

Parameters
----------
M : float or numpy.array
Total mass in solar mass units

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
return 1.0 / (3.0**(1.5) * lal.PI * M * lal.MTSUN_SI)

[docs]def f_ERD(M):
"""
Effective RingDown frequency studied in Pan et al. (arXiv:0704.1964)
found to give good fit between stationary-phase templates and
numerical relativity waveforms [NB equal-mass & nonspinning!]
Equal to 1.07*omega_220/2*pi

Parameters
----------
M : float or numpy.array
Total mass in solar mass units

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
return 1.07 * 0.5326 / (2*lal.PI * 0.955 * M * lal.MTSUN_SI)

[docs]def f_FRD(m1, m2):
"""
Fundamental RingDown frequency calculated from the Berti, Cardoso and
Will (gr-qc/0512160) value for the omega_220 QNM frequency using
mass-ratio dependent fits to the final BH mass and spin from Buonanno

Parameters
----------
m1 : float or numpy.array
First component mass in solar mass units
m2 : float or numpy.array
Second component mass in solar mass units

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
m_total, eta = mass1_mass2_to_mtotal_eta(m1, m2)
tmp = ( (1. - 0.63*(1. - 3.4641016*eta + 2.9*eta**2)**(0.3)) /
(1. - 0.057191*eta - 0.498*eta**2) )
return tmp / (2.*lal.PI * m_total*lal.MTSUN_SI)

[docs]def f_LRD(m1, m2):
"""
Lorentzian RingDown frequency = 1.2*FRD which captures part of
the Lorentzian tail from the decay of the QNMs

Parameters
----------
m1 : float or numpy.array
First component mass in solar mass units
m2 : float or numpy.array
Second component mass in solar mass units

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
return 1.2 * f_FRD(m1, m2)

def _get_freq(freqfunc, m1, m2, s1z, s2z):
"""Wrapper of the LALSimulation function returning the frequency
for a given frequency function and template parameters.

Parameters
----------
freqfunc : lalsimulation FrequencyFunction wrapped object e.g.
lalsimulation.fEOBNRv2RD
m1 : float-ish, i.e. castable to float
First component mass in solar masses
m2 : float-ish
Second component mass in solar masses
s1z : float-ish
First component dimensionless spin S_1/m_1^2 projected onto L
s2z : float-ish
Second component dimensionless spin S_2/m_2^2 projected onto L

Returns
-------
f : float
Frequency in Hz
"""
return lalsim.SimInspiralGetFrequency(
solar_mass_to_kg(m1),
solar_mass_to_kg(m2),
0,
0,
float(s1z),
0,
0,
float(s2z),
int(freqfunc)
)

# vectorize to enable calls with numpy arrays
_vec_get_freq = numpy.vectorize(_get_freq)

[docs]def get_freq(freqfunc, m1, m2, s1z, s2z):
"""
Returns the LALSimulation function which evaluates the frequency
for the given frequency function and template parameters.

Parameters
----------
freqfunc : string
Name of the frequency function to use, e.g., 'fEOBNRv2RD'
m1 : float or numpy.array
First component mass in solar masses
m2 : float or numpy.array
Second component mass in solar masses
s1z : float or numpy.array
First component dimensionless spin S_1/m_1^2 projected onto L
s2z : float or numpy.array
Second component dimensionless spin S_2/m_2^2 projected onto L

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
lalsim_ffunc = getattr(lalsim, freqfunc)
return _vec_get_freq(lalsim_ffunc, m1, m2, s1z, s2z)

def _get_final_freq(approx, m1, m2, s1z, s2z):
"""Wrapper of the LALSimulation function returning the final (highest)
frequency for a given approximant an template parameters

Parameters
----------
approx : lalsimulation approximant wrapped object e.g.
lalsimulation.EOBNRv2
m1 : float-ish, i.e. castable to float
First component mass in solar masses
m2 : float-ish
Second component mass in solar masses
s1z : float-ish
First component dimensionless spin S_1/m_1^2 projected onto L
s2z : float-ish
Second component dimensionless spin S_2/m_2^2 projected onto L

Returns
-------
f : float
Frequency in Hz
"""
return lalsim.SimInspiralGetFinalFreq(
solar_mass_to_kg(m1),
solar_mass_to_kg(m2),
0,
0,
float(s1z),
0,
0,
float(s2z),
int(approx)
)

# vectorize to enable calls with numpy arrays
_vec_get_final_freq = numpy.vectorize(_get_final_freq)

[docs]def get_final_freq(approx, m1, m2, s1z, s2z):
"""Returns the final (highest) frequency for a given approximant using
given template parameters.

NOTE: TaylorTx and TaylorFx are currently all given an ISCO cutoff !!

Parameters
----------
approx : string
Name of the approximant e.g. 'EOBNRv2'
m1 : float or numpy.array
First component mass in solar masses
m2 : float or numpy.array
Second component mass in solar masses
s1z : float or numpy.array
First component dimensionless spin S_1/m_1^2 projected onto L
s2z : float or numpy.array
Second component dimensionless spin S_2/m_2^2 projected onto L

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
# Unfortunately we need a few special cases (quite hacky in the case of
# IMRPhenomXAS) because some useful approximants are not understood by
# GetApproximantFromString().
if approx in ['IMRPhenomD', 'IMRPhenomXAS']:
return frequency_cutoff_from_name('IMRPhenomDPeak', m1, m2, s1z, s2z)
if approx == 'SEOBNRv5':
return frequency_cutoff_from_name('SEOBNRv5RD', m1, m2, s1z, s2z)
lalsim_approx = lalsim.GetApproximantFromString(approx)
return _vec_get_final_freq(lalsim_approx, m1, m2, s1z, s2z)

# Dictionary of functions with uniform API taking a
# parameter dict indexed on mass1, mass2, spin1z, spin2z
named_frequency_cutoffs = {
# functions depending on the total mass alone
"SchwarzISCO": lambda p: f_SchwarzISCO(p["mass1"]+p["mass2"]),
"LightRing"  : lambda p: f_LightRing(p["mass1"]+p["mass2"]),
"ERD"        : lambda p: f_ERD(p["mass1"]+p["mass2"]),
# functions depending on the 2 component masses
"BKLISCO"    : lambda p: f_BKLISCO(p["mass1"], p["mass2"]),
"FRD"        : lambda p: f_FRD(p["mass1"], p["mass2"]),
"LRD"        : lambda p: f_LRD(p["mass1"], p["mass2"]),
# functions depending on 2 component masses and aligned spins
"MECO"       : lambda p: meco_frequency(p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"HybridMECO" : lambda p: hybrid_meco_frequency(
p["mass1"], p["mass2"], p["spin1z"], p["spin2z"], qm1=None, qm2=None),
"IMRPhenomBFinal": lambda p: get_freq("fIMRPhenomBFinal",
p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"IMRPhenomCFinal": lambda p: get_freq("fIMRPhenomCFinal",
p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"IMRPhenomDPeak": lambda p: get_freq("fIMRPhenomDPeak",
p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"EOBNRv2RD"   : lambda p: get_freq("fEOBNRv2RD", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"EOBNRv2HMRD" : lambda p: get_freq("fEOBNRv2HMRD", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"SEOBNRv1RD"  : lambda p: get_freq("fSEOBNRv1RD",  p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"SEOBNRv1Peak": lambda p: get_freq("fSEOBNRv1Peak", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"SEOBNRv2RD": lambda p: get_freq("fSEOBNRv2RD", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"SEOBNRv2Peak": lambda p: get_freq("fSEOBNRv2Peak", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"SEOBNRv4RD": lambda p: get_freq("fSEOBNRv4RD", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"SEOBNRv4Peak": lambda p: get_freq("fSEOBNRv4Peak", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"SEOBNRv5RD": lambda p: get_freq("fSEOBNRv5RD", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"]),
"SEOBNRv5Peak": lambda p: get_freq("fSEOBNRv5Peak", p["mass1"], p["mass2"],
p["spin1z"], p["spin2z"])
}

[docs]def frequency_cutoff_from_name(name, m1, m2, s1z, s2z):
"""
Returns the result of evaluating the frequency cutoff function
specified by 'name' on a template with given parameters.

Parameters
----------
name : string
Name of the cutoff function
m1 : float or numpy.array
First component mass in solar masses
m2 : float or numpy.array
Second component mass in solar masses
s1z : float or numpy.array
First component dimensionless spin S_1/m_1^2 projected onto L
s2z : float or numpy.array
Second component dimensionless spin S_2/m_2^2 projected onto L

Returns
-------
f : float or numpy.array
Frequency in Hz
"""
params = {"mass1": m1, "mass2": m2, "spin1z": s1z, "spin2z": s2z}
return named_frequency_cutoffs[name](params)

def _get_imr_duration(m1, m2, s1z, s2z, f_low, approximant="SEOBNRv4"):
"""Wrapper of lalsimulation template duration approximate formula"""
m1, m2, s1z, s2z, f_low = float(m1), float(m2), float(s1z), float(s2z),\
float(f_low)
if approximant == "SEOBNRv2":
chi = lalsim.SimIMRPhenomBComputeChi(m1, m2, s1z, s2z)
time_length = lalsim.SimIMRSEOBNRv2ChirpTimeSingleSpin(
m1 * lal.MSUN_SI, m2 * lal.MSUN_SI, chi, f_low)
elif approximant == "IMRPhenomXAS":
time_length = lalsim.SimIMRPhenomXASDuration(
m1 * lal.MSUN_SI, m2 * lal.MSUN_SI, s1z, s2z, f_low)
elif approximant == "IMRPhenomD":
time_length = lalsim.SimIMRPhenomDChirpTime(
m1 * lal.MSUN_SI, m2 * lal.MSUN_SI, s1z, s2z, f_low)
elif approximant in ["SEOBNRv4", "SEOBNRv4_ROM"]:
# NB the LALSim function has f_low as first argument
time_length = lalsim.SimIMRSEOBNRv4ROMTimeOfFrequency(
f_low, m1 * lal.MSUN_SI, m2 * lal.MSUN_SI, s1z, s2z)
elif approximant in ["SEOBNRv5", "SEOBNRv5_ROM"]:
time_length = lalsim.SimIMRSEOBNRv5ROMTimeOfFrequency(
f_low, m1 * lal.MSUN_SI, m2 * lal.MSUN_SI, s1z, s2z)
elif approximant in ["SPAtmplt", "TaylorF2"]:
chi = lalsim.SimInspiralTaylorF2ReducedSpinComputeChi(
m1, m2, s1z, s2z
)
time_length = lalsim.SimInspiralTaylorF2ReducedSpinChirpTime(
f_low, m1 * lal.MSUN_SI, m2 * lal.MSUN_SI, chi, -1
)
else:
raise RuntimeError("I can't calculate a duration for %s" % approximant)
# FIXME Add an extra factor of 1.1 for 'safety' since the duration
# functions are approximate
return time_length * 1.1

get_imr_duration = numpy.vectorize(_get_imr_duration)

[docs]def get_inspiral_tf(tc, mass1, mass2, spin1, spin2, f_low, n_points=100,
pn_2order=7, approximant='TaylorF2'):
"""Compute the time-frequency evolution of an inspiral signal.

Return a tuple of time and frequency vectors tracking the evolution of an
inspiral signal in the time-frequency plane.
"""
# handle param-dependent approximant specification
class Params:
pass
params = Params()
params.mass1 = mass1
params.mass2 = mass2
params.spin1z = spin1
params.spin2z = spin2
try:
approximant = eval(approximant, {'__builtins__': None},
dict(params=params))
except (NameError, TypeError):
pass

if approximant in ['TaylorF2', 'SPAtmplt']:
from pycbc.waveform.spa_tmplt import findchirp_chirptime

# FIXME spins are not taken into account
f_high = f_SchwarzISCO(mass1 + mass2)
def tof_func(f):
return findchirp_chirptime(
float(mass1),
float(mass2),
float(f),
pn_2order
)
elif approximant.startswith('SEOBNRv'):
approximant_prefix = approximant[:len('SEOBNRv*')]
f_high = get_final_freq(approximant_prefix, mass1, mass2, spin1, spin2)
f_high *= 0.999  # avoid errors due to rounding
tof_func_map = {
# use HI function for v2 as it has wider freq range validity
'SEOBNRv2': lalsim.SimIMRSEOBNRv2ROMDoubleSpinHITimeOfFrequency,
'SEOBNRv4': lalsim.SimIMRSEOBNRv4ROMTimeOfFrequency,
'SEOBNRv5': lalsim.SimIMRSEOBNRv5ROMTimeOfFrequency
}
def tof_func(f):
f,
solar_mass_to_kg(mass1),
solar_mass_to_kg(mass2),
float(spin1),
float(spin2)
)
elif approximant in ['IMRPhenomD', 'IMRPhenomXAS']:
f_high = get_final_freq(approximant, mass1, mass2, spin1, spin2)
tof_func_map = {
'IMRPhenomD': lalsim.SimIMRPhenomDChirpTime,
'IMRPhenomXAS': lalsim.SimIMRPhenomXASDuration
}
def tof_func(f):
solar_mass_to_kg(mass1),
solar_mass_to_kg(mass2),
float(spin1),
float(spin2),
f
)
else:
raise ValueError(f'Approximant {approximant} not supported')
track_f = numpy.logspace(numpy.log10(f_low), numpy.log10(f_high), n_points)
tof_func_vec = numpy.vectorize(tof_func)
track_t = tc - tof_func_vec(track_f)
return (track_t, track_f)

##############################This code was taken from Andy ###########

def _energy_coeffs(m1, m2, chi1, chi2):
""" Return the center-of-mass energy coefficients up to 3.0pN (2.5pN spin)
"""
mtot = m1 + m2
eta = m1*m2 / (mtot*mtot)
chi = (m1*chi1 + m2*chi2) / mtot
chisym = (chi1 + chi2) / 2.
beta = (113.*chi - 76.*eta*chisym)/12.
sigma12 = 79.*eta*chi1*chi2/8.
sigmaqm = 81.*m1*m1*chi1*chi1/(16.*mtot*mtot) \
+ 81.*m2*m2*chi2*chi2/(16.*mtot*mtot)

energy0 = -0.5*eta
energy2 = -0.75 - eta/12.
energy3 = 0.
energy4 = -3.375 + (19*eta)/8. - pow(eta,2)/24.
energy5 = 0.
energy6 = -10.546875 - (155*pow(eta,2))/96. - (35*pow(eta,3))/5184. \
+ eta*(59.80034722222222 - (205*pow(lal.PI,2))/96.)

energy3 += (32*beta)/113. + (52*chisym*eta)/113.

energy4 += (-16*sigma12)/79. - (16*sigmaqm)/81.
energy5 += (96*beta)/113. + ((-124*beta)/339. - (522*chisym)/113.)*eta \
- (710*chisym*pow(eta,2))/339.

return (energy0, energy2, energy3, energy4, energy5, energy6)

[docs]def meco_velocity(m1, m2, chi1, chi2):
"""
Returns the velocity of the minimum energy cutoff for 3.5pN (2.5pN spin)

Parameters
----------
m1 : float
First component mass in solar masses
m2 : float
Second component mass in solar masses
chi1 : float
First component dimensionless spin S_1/m_1^2 projected onto L
chi2 : float
Second component dimensionless spin S_2/m_2^2 projected onto L

Returns
-------
v : float
Velocity (dimensionless)
"""
_, energy2, energy3, energy4, energy5, energy6 = \
_energy_coeffs(m1, m2, chi1, chi2)
def eprime(v):
return 2. + v * v * (4.*energy2 + v * (5.*energy3 \
+ v * (6.*energy4
+ v * (7.*energy5 + 8.*energy6 * v))))
return bisect(eprime, 0.05, 1.0)

def _meco_frequency(m1, m2, chi1, chi2):
"""Returns the frequency of the minimum energy cutoff for 3.5pN (2.5pN spin)
"""
return velocity_to_frequency(meco_velocity(m1, m2, chi1, chi2), m1+m2)

meco_frequency = numpy.vectorize(_meco_frequency)

def _dtdv_coeffs(m1, m2, chi1, chi2):
""" Returns the dt/dv coefficients up to 3.5pN (2.5pN spin)
"""
mtot = m1 + m2
eta = m1*m2 / (mtot*mtot)
chi = (m1*chi1 + m2*chi2) / mtot
chisym = (chi1 + chi2) / 2.
beta = (113.*chi - 76.*eta*chisym)/12.
sigma12 = 79.*eta*chi1*chi2/8.
sigmaqm = 81.*m1*m1*chi1*chi1/(16.*mtot*mtot) \
+ 81.*m2*m2*chi2*chi2/(16.*mtot*mtot)

dtdv0 = 1. # FIXME: Wrong but doesn't matter for now.
dtdv2 = (1./336.) * (743. + 924.*eta)
dtdv3 = -4. * lal.PI + beta
dtdv4 = (3058673. + 5472432.*eta + 4353552.*eta*eta)/1016064. - sigma12 - sigmaqm
dtdv5 = (1./672.) * lal.PI * (-7729. + 1092.*eta) + (146597.*beta/18984. + 42.*beta*eta/113. - 417307.*chisym*eta/18984. - 1389.*chisym*eta*eta/226.)
dtdv6 = 22.065 + 165.416*eta - 2.20067*eta*eta + 4.93152*eta*eta*eta
dtdv6log = 1712./315.
dtdv7 = (lal.PI/1016064.) * (-15419335. - 12718104.*eta + 4975824.*eta*eta)

return (dtdv0, dtdv2, dtdv3, dtdv4, dtdv5, dtdv6, dtdv6log, dtdv7)

def _dtdv_cutoff_velocity(m1, m2, chi1, chi2):
_, dtdv2, dtdv3, dtdv4, dtdv5, dtdv6, dtdv6log, dtdv7 = _dtdv_coeffs(m1, m2, chi1, chi2)

def dtdv_func(v):
x = dtdv7
x = v * x + dtdv6 + dtdv6log * 3. * numpy.log(v)
x = v * x + dtdv5
x = v * x + dtdv4
x = v * x + dtdv3
x = v * x + dtdv2
return v * v * x + 1.

if dtdv_func(1.0) < 0.:
return bisect(dtdv_func, 0.05, 1.0)
else:
return 1.0

[docs]def energy_coefficients(m1, m2, s1z=0, s2z=0, phase_order=-1, spin_order=-1):
""" Return the energy coefficients. This assumes that the system has aligned spins only.
"""
implemented_phase_order = 7
implemented_spin_order = 7
if phase_order > implemented_phase_order:
raise ValueError("pN coeffiecients of that order have not been implemented")
elif phase_order == -1:
phase_order = implemented_phase_order

if spin_order > implemented_spin_order:
raise ValueError("pN coeffiecients of that order have not been implemented")
elif spin_order == -1:
spin_order = implemented_spin_order

qmdef1 = 1.0
qmdef2 = 1.0

M = m1 + m2
dm = (m1-m2)/M
m1M = m1 / M
m2M = m2 / M

s1z = s1z * m1M * m1M
s2z = s2z * m2M * m2M

_, eta = mass1_mass2_to_mchirp_eta(m1, m2)

ecof = numpy.zeros(phase_order+1)
# Orbital terms
if phase_order >= 0:
ecof[0] = 1.0
if phase_order >= 1:
ecof[1] = 0
if phase_order >= 2:
ecof[2] = -(1.0/12.0) * (9.0 + eta)
if phase_order >= 3:
ecof[3] = 0
if phase_order >= 4:
ecof[4] = (-81.0 + 57.0*eta - eta*eta) / 24.0
if phase_order >= 5:
ecof[5] = 0
if phase_order >= 6:
ecof[6] = - 675.0/64.0 + ( 34445.0/576.0    \
- 205.0/96.0 * lal.PI * lal.PI ) * eta  \
- (155.0/96.0) *eta * eta - 35.0/5184.0 * eta * eta
# Spin terms

ESO15s1 = 8.0/3.0 + 2.0*m2/m1
ESO15s2 = 8.0/3.0 + 2.0*m1/m2

ESS2 = 1.0 / eta
EQM2s1 = qmdef1/2.0/m1M/m1M
EQM2s1L = -qmdef1*3.0/2.0/m1M/m1M
#EQM2s2 = qmdef2/2.0/m2M/m2M
EQM2s2L = -qmdef2*3.0/2.0/m2M/m2M

ESO25s1 = 11.0 - 61.0*eta/9.0 + (dm/m1M) * (-3.0 + 10.*eta/3.0)
ESO25s2 = 11.0 - 61.0*eta/9.0 + (dm/m2M) * (3.0 - 10.*eta/3.0)

ESO35s1 = 135.0/4.0 - 367.0*eta/4.0 + 29.0*eta*eta/12.0 + (dm/m1M) * (-27.0/4.0 + 39.0*eta - 5.0*eta*eta/4.0)
ESO35s2 = 135.0/4.0 - 367.0*eta/4.0 + 29.0*eta*eta/12.0 - (dm/m2M) * (-27.0/4.0 + 39.0*eta - 5.0*eta*eta/4.0)

if spin_order >=3:
ecof[3] += ESO15s1 * s1z + ESO15s2 * s2z
if spin_order >=4:
ecof[4] += ESS2 * (s1z*s2z - 3.0*s1z*s2z)
ecof[4] += EQM2s1*s1z*s1z + EQM2s1*s2z*s2z + EQM2s1L*s1z*s1z + EQM2s2L*s2z*s2z
if spin_order >=5:
ecof[5] = ESO25s1*s1z + ESO25s2*s2z
if spin_order >=7:
ecof[7] += ESO35s1*s1z + ESO35s2*s2z

return ecof

[docs]def energy(v, mass1, mass2, s1z=0, s2z=0, phase_order=-1, spin_order=-1):
ecof = energy_coefficients(mass1, mass2, s1z, s2z, phase_order, spin_order)
_, eta = mass1_mass2_to_mchirp_eta(mass1, mass2)
amp = - (1.0/2.0) * eta
e = 0.0
for i in numpy.arange(0, len(ecof), 1):
e += v**(i+2.0) * ecof[i]

return e * amp

[docs]def meco2(m1, m2, s1z=0, s2z=0, phase_order=-1, spin_order=-1):
ecof = energy_coefficients(m1, m2, s1z, s2z, phase_order, spin_order)

def test(v):
de = 0
for i in numpy.arange(0, len(ecof), 1):
de += v**(i+1.0)* ecof[i] * (i + 2)

return de

return bisect(test, 0.001, 1.0)

[docs]def t2_cutoff_velocity(m1, m2, chi1, chi2):
return min(meco_velocity(m1,m2,chi1,chi2), _dtdv_cutoff_velocity(m1,m2,chi1,chi2))

[docs]def t2_cutoff_frequency(m1, m2, chi1, chi2):
return velocity_to_frequency(t2_cutoff_velocity(m1, m2, chi1, chi2), m1 + m2)

t4_cutoff_velocity = meco_velocity
t4_cutoff_frequency = meco_frequency

# Hybrid MECO in arXiv:1602.03134
# To obtain the MECO, find minimum in v of eq. (6)

[docs]def kerr_lightring(v, chi):
"""Return the function whose first root defines the Kerr light ring"""
return 1 + chi * v**3 - 3 * v**2 * (1 - chi * v**3)**(1./3)

[docs]def kerr_lightring_velocity(chi):
"""Return the velocity at the Kerr light ring"""
# If chi > 0.9996, the algorithm cannot solve the function
if chi >= 0.9996:
return brentq(kerr_lightring, 0, 0.8, args=(0.9996))
else:
return brentq(kerr_lightring, 0, 0.8, args=(chi))

[docs]def hybridEnergy(v, m1, m2, chi1, chi2, qm1, qm2):
"""Return hybrid MECO energy.

Return the hybrid energy [eq. (6)] whose minimum defines the hybrid MECO
up to 3.5PN (including the 3PN spin-spin)

Parameters
----------
m1 : float
Mass of the primary object in solar masses.
m2 : float
Mass of the secondary object in solar masses.
chi1: float
Dimensionless spin of the primary object.
chi2: float
Dimensionless spin of the secondary object.
qm1: float
Quadrupole-monopole term of the primary object (1 for black holes).
qm2: float
Quadrupole-monopole term of the secondary object (1 for black holes).

Returns
-------
h_E: float
The hybrid energy as a function of v
"""
pi_sq = numpy.pi**2
v2, v3, v4, v5, v6, v7 = v**2, v**3, v**4, v**5, v**6, v**7
chi1_sq, chi2_sq = chi1**2, chi2**2
m1, m2 = float(m1), float(m2)
M = float(m1 + m2)
M_2, M_4 = M**2, M**4
eta = m1 * m2 / M_2
eta2, eta3 = eta**2, eta**3
m1_2, m1_4 = m1**2, m1**4
m2_2, m2_4 = m2**2, m2**4

chi = (chi1 * m1 + chi2 * m2) / M
Kerr = -1. + (1. - 2. * v2 * (1. - chi * v3)**(1./3.)) / \
numpy.sqrt((1. - chi * v3) * (1. + chi * v3 - 3. * v2 * (1 - chi * v3)**(1./3.)))

h_E = Kerr - \
(v2 / 2.) * \
(
- eta * v2 / 12. - 2 * (chi1 + chi2) * eta * v3 / 3. +
(19. * eta / 8. - eta2 / 24. + chi1_sq * m1_2 * (1 - qm1) / M_2 +
chi2_sq * m2_2 * (1 - qm2) / M_2) * v4
- 1. / 9. * (120. * (chi1 + chi2) * eta2 +
(76. * chi1 + 45. * chi2) * m1_2 * eta / M_2 +
(45. * chi1 + 76. * chi2) * m2_2 * eta / M_2) * v5
+ (34445. * eta / 576. - 205. * pi_sq * eta / 96. - 155. * eta2 / 96. -
35. * eta3 / 5184. +
5. / 18. * (21. * chi1_sq * (1. - qm1) * m1_4 / M_4 +
21. * chi2_sq * (1. - qm2) * m2_4 / M_4 +
(chi1_sq * (56. - 27. * qm1) + 20. * chi1 * chi2) * eta * m1_2 / M_2 +
(chi2_sq * (56. - 27. * qm2) + 20. * chi1 * chi2) * eta * m2_2 / M_2 +
(chi1_sq * (31. - 9. * qm1) + 38. * chi1 * chi2 +
chi2_sq * (31. - 9. * qm2)) * eta2)) * v6
- eta / 12. * (3. * (292. * chi1 + 81. * chi2) * m1_4 / M_4 +
3. * (81. * chi1 + 292. * chi2) * m2_4 / M_4 +
4. * (673. * chi1 + 360. * chi2) * eta * m1_2 / M_2 +
4. * (360. * chi1 + 673. * chi2) * eta * m2_2 / M_2 +
3012. * eta2 * (chi1 + chi2)) * v7
)

return h_E

[docs]def hybrid_meco_velocity(m1, m2, chi1, chi2, qm1=None, qm2=None):
"""Return the velocity of the hybrid MECO

Parameters
----------
m1 : float
Mass of the primary object in solar masses.
m2 : float
Mass of the secondary object in solar masses.
chi1: float
Dimensionless spin of the primary object.
chi2: float
Dimensionless spin of the secondary object.
qm1: {None, float}, optional
Quadrupole-monopole term of the primary object (1 for black holes).
If None, will be set to qm1 = 1.
qm2: {None, float}, optional
Quadrupole-monopole term of the secondary object (1 for black holes).
If None, will be set to qm2 = 1.

Returns
-------
v: float
The velocity (dimensionless) of the hybrid MECO
"""

if qm1 is None:
qm1 = 1
if qm2 is None:
qm2 = 1

# Set bounds at 0.1 to skip v=0 and at the lightring velocity
chi = (chi1 * m1 + chi2 * m2) / (m1 + m2)
vmax = kerr_lightring_velocity(chi) - 0.01

return minimize(hybridEnergy, 0.2, args=(m1, m2, chi1, chi2, qm1, qm2),
bounds=[(0.1, vmax)]).x.item()

[docs]def hybrid_meco_frequency(m1, m2, chi1, chi2, qm1=None, qm2=None):
"""Return the frequency of the hybrid MECO

Parameters
----------
m1 : float
Mass of the primary object in solar masses.
m2 : float
Mass of the secondary object in solar masses.
chi1: float
Dimensionless spin of the primary object.
chi2: float
Dimensionless spin of the secondary object.
qm1: {None, float}, optional
Quadrupole-monopole term of the primary object (1 for black holes).
If None, will be set to qm1 = 1.
qm2: {None, float}, optional
Quadrupole-monopole term of the secondary object (1 for black holes).
If None, will be set to qm2 = 1.

Returns
-------
f: float
The frequency (in Hz) of the hybrid MECO
"""
if qm1 is None:
qm1 = 1
if qm2 is None:
qm2 = 1

return velocity_to_frequency(hybrid_meco_velocity(m1, m2, chi1, chi2, qm1, qm2), m1 + m2)

[docs]def jframe_to_l0frame(mass1, mass2, f_ref, phiref=0., thetajn=0., phijl=0.,
spin1_a=0., spin2_a=0.,
spin1_polar=0., spin2_polar=0.,
spin12_deltaphi=0.):
"""Converts J-frame parameters into L0 frame.

Parameters
----------
mass1 : float
The mass of the first component object in the
binary (in solar masses)
mass2 : float
The mass of the second component object in the
binary (in solar masses)
f_ref : float
The reference frequency.
thetajn : float
Angle between the line of sight and the total angular momentume J.
phijl : float
Azimuthal angle of L on its cone about J.
spin1_a : float
The dimensionless spin magnitude :math:|\\vec{{s}}_1/m^2_1|.
spin2_a : float
The dimensionless spin magnitude :math:|\\vec{{s}}_2/m^2_2|.
spin1_polar : float
Angle between L and the spin magnitude of the larger object.
spin2_polar : float
Angle betwen L and the spin magnitude of the smaller object.
spin12_deltaphi : float
Difference between the azimuthal angles of the spin of the larger
object (S1) and the spin of the smaller object (S2).

Returns
-------
dict :
Dictionary of:

* inclination : float
Inclination (rad), defined as the angle between
the orbital angular momentum L and the
line-of-sight at the reference frequency.
* spin1x : float
The x component of the first binary component's
dimensionless spin.
* spin1y : float
The y component of the first binary component's
dimensionless spin.
* spin1z : float
The z component of the first binary component's
dimensionless spin.
* spin2x : float
The x component of the second binary component's
dimensionless spin.
* spin2y : float
The y component of the second binary component's
dimensionless spin.
* spin2z : float
The z component of the second binary component's
dimensionless spin.
"""
inclination, spin1x, spin1y, spin1z, spin2x, spin2y, spin2z = \
lalsim.SimInspiralTransformPrecessingNewInitialConditions(
thetajn, phijl, spin1_polar, spin2_polar, spin12_deltaphi,
spin1_a, spin2_a, mass1*lal.MSUN_SI, mass2*lal.MSUN_SI, f_ref,
phiref)
out = {'inclination': inclination,
'spin1x': spin1x,
'spin1y': spin1y,
'spin1z': spin1z,
'spin2x': spin2x,
'spin2y': spin2y,
'spin2z': spin2z}
return out

[docs]def l0frame_to_jframe(mass1, mass2, f_ref, phiref=0., inclination=0.,
spin1x=0., spin1y=0., spin1z=0.,
spin2x=0., spin2y=0., spin2z=0.):
"""Converts L0-frame parameters to J-frame.

Parameters
----------
mass1 : float
The mass of the first component object in the
binary (in solar masses)
mass2 : float
The mass of the second component object in the
binary (in solar masses)
f_ref : float
The reference frequency.
phiref : float
The orbital phase at f_ref.
inclination : float
Inclination (rad), defined as the angle between
the orbital angular momentum L and the
line-of-sight at the reference frequency.
spin1x : float
The x component of the first binary component's
dimensionless spin.
spin1y : float
The y component of the first binary component's
dimensionless spin.
spin1z : float
The z component of the first binary component's
dimensionless spin.
spin2x : float
The x component of the second binary component's
dimensionless spin.
spin2y : float
The y component of the second binary component's
dimensionless spin.
spin2z : float
The z component of the second binary component's
dimensionless spin.

Returns
-------
dict :
Dictionary of:

* thetajn : float
Angle between the line of sight and the total angular momentume J.
* phijl : float
Azimuthal angle of L on its cone about J.
* spin1_a : float
The dimensionless spin magnitude :math:|\\vec{{s}}_1/m^2_1|.
* spin2_a : float
The dimensionless spin magnitude :math:|\\vec{{s}}_2/m^2_2|.
* spin1_polar : float
Angle between L and the spin magnitude of the larger object.
* spin2_polar : float
Angle betwen L and the spin magnitude of the smaller object.
* spin12_deltaphi : float
Difference between the azimuthal angles of the spin of the larger
object (S1) and the spin of the smaller object (S2).
"""
# Note: unlike other LALSimulation functions, this one takes masses in
# solar masses
thetajn, phijl, s1pol, s2pol, s12_deltaphi, spin1_a, spin2_a = \
lalsim.SimInspiralTransformPrecessingWvf2PE(
inclination, spin1x, spin1y, spin1z, spin2x, spin2y, spin2z,
mass1, mass2, f_ref, phiref)
out = {'thetajn': thetajn,
'phijl': phijl,
'spin1_polar': s1pol,
'spin2_polar': s2pol,
'spin12_deltaphi': s12_deltaphi,
'spin1_a': spin1_a,
'spin2_a': spin2_a}
return out