Source code for pycbc.pnutils

# Copyright (C) 2012  Alex Nitz
#
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# 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 lal
import numpy
from scipy.optimize import bisect, brentq, minimize
from pycbc import conversions, libutils

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 (arXiv:0709.3839). See also arxiv:0801.4297v2 eq.(5) 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 et al. (arXiv:0706.3732) : see also InspiralBankGeneration.c 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): return tof_func_map[approximant_prefix]( 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): return tof_func_map[approximant]( 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