Source code for pycbc.conversions

# Copyright (C) 2017  Collin Capano, Christopher M. Biwer, Duncan Brown,
# and Steven Reyes
# 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 provides a library of functions that calculate waveform parameters
from other parameters. All exposed functions in this module's namespace return
one parameter given a set of inputs.
"""

import copy
import numpy
import logging

import lal

from pycbc.detector import Detector
import pycbc.cosmology
from pycbc import neutron_stars as ns

from .coordinates import (
    spherical_to_cartesian as _spherical_to_cartesian,
    cartesian_to_spherical as _cartesian_to_spherical)

pykerr = pycbc.libutils.import_optional('pykerr')
lalsim = pycbc.libutils.import_optional('lalsimulation')

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

#
# =============================================================================
#
#                           Helper functions
#
# =============================================================================
#
def ensurearray(*args):
    """Apply numpy's broadcast rules to the given arguments.

    This will ensure that all of the arguments are numpy arrays and that they
    all have the same shape. See ``numpy.broadcast_arrays`` for more details.

    It also returns a boolean indicating whether any of the inputs were
    originally arrays.

    Parameters
    ----------
    *args :
        The arguments to check.

    Returns
    -------
    list :
        A list with length ``N+1`` where ``N`` is the number of given
        arguments. The first N values are the input arguments as ``ndarrays``s.
        The last value is a boolean indicating whether any of the
        inputs was an array.
    """
    input_is_array = any(isinstance(arg, numpy.ndarray) for arg in args)
    args = list(numpy.broadcast_arrays(*args))
    args.append(input_is_array)
    return tuple(args)


def formatreturn(arg, input_is_array=False):
    """If the given argument is a numpy array with shape (1,), just returns
    that value."""
    if not input_is_array and arg.size == 1:
        arg = arg.item()
    return arg

#
# =============================================================================
#
#                           Fundamental conversions
#
# =============================================================================
#

def sec_to_year(sec):
    """ Converts number of seconds to number of years """
    return sec / lal.YRJUL_SI


#
# =============================================================================
#
#                           CBC mass functions
#
# =============================================================================
#
[docs] def primary_mass(mass1, mass2): """Returns the larger of mass1 and mass2 (p = primary).""" mass1, mass2, input_is_array = ensurearray(mass1, mass2) if mass1.shape != mass2.shape: raise ValueError("mass1 and mass2 must have same shape") mp = copy.copy(mass1) mask = mass1 < mass2 mp[mask] = mass2[mask] return formatreturn(mp, input_is_array)
[docs] def secondary_mass(mass1, mass2): """Returns the smaller of mass1 and mass2 (s = secondary).""" mass1, mass2, input_is_array = ensurearray(mass1, mass2) if mass1.shape != mass2.shape: raise ValueError("mass1 and mass2 must have same shape") ms = copy.copy(mass2) mask = mass1 < mass2 ms[mask] = mass1[mask] return formatreturn(ms, input_is_array)
[docs] def mtotal_from_mass1_mass2(mass1, mass2): """Returns the total mass from mass1 and mass2.""" return mass1 + mass2
[docs] def q_from_mass1_mass2(mass1, mass2): """Returns the mass ratio m1/m2, where m1 >= m2.""" return primary_mass(mass1, mass2) / secondary_mass(mass1, mass2)
[docs] def invq_from_mass1_mass2(mass1, mass2): """Returns the inverse mass ratio m2/m1, where m1 >= m2.""" return secondary_mass(mass1, mass2) / primary_mass(mass1, mass2)
[docs] def eta_from_mass1_mass2(mass1, mass2): """Returns the symmetric mass ratio from mass1 and mass2.""" return mass1*mass2 / (mass1 + mass2)**2.
[docs] def mchirp_from_mass1_mass2(mass1, mass2): """Returns the chirp mass from mass1 and mass2.""" return eta_from_mass1_mass2(mass1, mass2)**(3./5) * (mass1 + mass2)
[docs] def mass1_from_mtotal_q(mtotal, q): """Returns a component mass from the given total mass and mass ratio. If the mass ratio q is >= 1, the returned mass will be the primary (heavier) mass. If q < 1, the returned mass will be the secondary (lighter) mass. """ return q*mtotal / (1. + q)
[docs] def mass2_from_mtotal_q(mtotal, q): """Returns a component mass from the given total mass and mass ratio. If the mass ratio q is >= 1, the returned mass will be the secondary (lighter) mass. If q < 1, the returned mass will be the primary (heavier) mass. """ return mtotal / (1. + q)
[docs] def mass1_from_mtotal_eta(mtotal, eta): """Returns the primary mass from the total mass and symmetric mass ratio. """ return 0.5 * mtotal * (1.0 + (1.0 - 4.0 * eta)**0.5)
[docs] def mass2_from_mtotal_eta(mtotal, eta): """Returns the secondary mass from the total mass and symmetric mass ratio. """ return 0.5 * mtotal * (1.0 - (1.0 - 4.0 * eta)**0.5)
[docs] def mtotal_from_mchirp_eta(mchirp, eta): """Returns the total mass from the chirp mass and symmetric mass ratio. """ return mchirp / eta**(3./5.)
[docs] def mass1_from_mchirp_eta(mchirp, eta): """Returns the primary mass from the chirp mass and symmetric mass ratio. """ mtotal = mtotal_from_mchirp_eta(mchirp, eta) return mass1_from_mtotal_eta(mtotal, eta)
[docs] def mass2_from_mchirp_eta(mchirp, eta): """Returns the primary mass from the chirp mass and symmetric mass ratio. """ mtotal = mtotal_from_mchirp_eta(mchirp, eta) return mass2_from_mtotal_eta(mtotal, eta)
def _mass2_from_mchirp_mass1(mchirp, mass1): r"""Returns the secondary mass from the chirp mass and primary 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: .. math:: m_2^3 - a(m_2 + m_1) = 0, where .. math:: a = \frac{\mathcal{M}^5}{m_1^3}. This has 3 solutions but only one will be real. """ a = mchirp**5 / mass1**3 roots = numpy.roots([1, 0, -a, -a * mass1]) # Find the real one real_root = roots[(abs(roots - roots.real)).argmin()] return real_root.real mass2_from_mchirp_mass1 = numpy.vectorize(_mass2_from_mchirp_mass1) def _mass_from_knownmass_eta(known_mass, eta, known_is_secondary=False, force_real=True): r"""Returns the other component mass given one of the component masses and the symmetric mass ratio. This requires finding the roots of the quadratic equation: .. math:: \eta m_2^2 + (2\eta - 1)m_1 m_2 + \eta m_1^2 = 0. This has two solutions which correspond to :math:`m_1` being the heavier mass or it being the lighter mass. By default, `known_mass` is assumed to be the heavier (primary) mass, and the smaller solution is returned. Use the `other_is_secondary` to invert. Parameters ---------- known_mass : float The known component mass. eta : float The symmetric mass ratio. known_is_secondary : {False, bool} Whether the known component mass is the primary or the secondary. If True, `known_mass` is assumed to be the secondary (lighter) mass and the larger solution is returned. Otherwise, the smaller solution is returned. Default is False. force_real : {True, bool} Force the returned mass to be real. Returns ------- float The other component mass. """ roots = numpy.roots([eta, (2*eta - 1) * known_mass, eta * known_mass**2.]) if force_real: roots = numpy.real(roots) if known_is_secondary: return roots[roots.argmax()] else: return roots[roots.argmin()] mass_from_knownmass_eta = numpy.vectorize(_mass_from_knownmass_eta)
[docs] def mass2_from_mass1_eta(mass1, eta, force_real=True): """Returns the secondary mass from the primary mass and symmetric mass ratio. """ return mass_from_knownmass_eta(mass1, eta, known_is_secondary=False, force_real=force_real)
[docs] def mass1_from_mass2_eta(mass2, eta, force_real=True): """Returns the primary mass from the secondary mass and symmetric mass ratio. """ return mass_from_knownmass_eta(mass2, eta, known_is_secondary=True, force_real=force_real)
[docs] def eta_from_q(q): r"""Returns the symmetric mass ratio from the given mass ratio. This is given by: .. math:: \eta = \frac{q}{(1+q)^2}. Note that the mass ratio may be either < 1 or > 1. """ return q / (1. + q)**2
[docs] def mass1_from_mchirp_q(mchirp, q): """Returns the primary mass from the given chirp mass and mass ratio.""" mass1 = q**(2./5.) * (1.0 + q)**(1./5.) * mchirp return mass1
[docs] def mass2_from_mchirp_q(mchirp, q): """Returns the secondary mass from the given chirp mass and mass ratio.""" mass2 = q**(-3./5.) * (1.0 + q)**(1./5.) * mchirp return mass2
def _a0(f_lower): """Used in calculating chirp times: see Cokelaer, arxiv.org:0706.4437 appendix 1, also lalinspiral/python/sbank/tau0tau3.py. """ return 5. / (256. * (numpy.pi * f_lower)**(8./3.)) def _a3(f_lower): """Another parameter used for chirp times""" return numpy.pi / (8. * (numpy.pi * f_lower)**(5./3.))
[docs] def tau0_from_mtotal_eta(mtotal, eta, f_lower): r"""Returns :math:`\tau_0` from the total mass, symmetric mass ratio, and the given frequency. """ # convert to seconds mtotal = mtotal * lal.MTSUN_SI # formulae from arxiv.org:0706.4437 return _a0(f_lower) / (mtotal**(5./3.) * eta)
[docs] def tau0_from_mchirp(mchirp, f_lower): r"""Returns :math:`\tau_0` from the chirp mass and the given frequency. """ # convert to seconds mchirp = mchirp * lal.MTSUN_SI # formulae from arxiv.org:0706.4437 return _a0(f_lower) / mchirp ** (5./3.)
[docs] def tau3_from_mtotal_eta(mtotal, eta, f_lower): r"""Returns :math:`\tau_0` from the total mass, symmetric mass ratio, and the given frequency. """ # convert to seconds mtotal = mtotal * lal.MTSUN_SI # formulae from arxiv.org:0706.4437 return _a3(f_lower) / (mtotal**(2./3.) * eta)
[docs] def tau0_from_mass1_mass2(mass1, mass2, f_lower): r"""Returns :math:`\tau_0` from the component masses and given frequency. """ mtotal = mass1 + mass2 eta = eta_from_mass1_mass2(mass1, mass2) return tau0_from_mtotal_eta(mtotal, eta, f_lower)
[docs] def tau3_from_mass1_mass2(mass1, mass2, f_lower): r"""Returns :math:`\tau_3` from the component masses and given frequency. """ mtotal = mass1 + mass2 eta = eta_from_mass1_mass2(mass1, mass2) return tau3_from_mtotal_eta(mtotal, eta, f_lower)
[docs] def mchirp_from_tau0(tau0, f_lower): r"""Returns chirp mass from :math:`\tau_0` and the given frequency. """ mchirp = (_a0(f_lower) / tau0) ** (3./5.) # in seconds # convert back to solar mass units return mchirp / lal.MTSUN_SI
[docs] def mtotal_from_tau0_tau3(tau0, tau3, f_lower, in_seconds=False): r"""Returns total mass from :math:`\tau_0, \tau_3`.""" mtotal = (tau3 / _a3(f_lower)) / (tau0 / _a0(f_lower)) if not in_seconds: # convert back to solar mass units mtotal /= lal.MTSUN_SI return mtotal
[docs] def eta_from_tau0_tau3(tau0, tau3, f_lower): r"""Returns symmetric mass ratio from :math:`\tau_0, \tau_3`.""" mtotal = mtotal_from_tau0_tau3(tau0, tau3, f_lower, in_seconds=True) eta = mtotal**(-2./3.) * (_a3(f_lower) / tau3) return eta
[docs] def mass1_from_tau0_tau3(tau0, tau3, f_lower): r"""Returns the primary mass from the given :math:`\tau_0, \tau_3`.""" mtotal = mtotal_from_tau0_tau3(tau0, tau3, f_lower) eta = eta_from_tau0_tau3(tau0, tau3, f_lower) return mass1_from_mtotal_eta(mtotal, eta)
[docs] def mass2_from_tau0_tau3(tau0, tau3, f_lower): r"""Returns the secondary mass from the given :math:`\tau_0, \tau_3`.""" mtotal = mtotal_from_tau0_tau3(tau0, tau3, f_lower) eta = eta_from_tau0_tau3(tau0, tau3, f_lower) return mass2_from_mtotal_eta(mtotal, eta)
[docs] def lambda_tilde(mass1, mass2, lambda1, lambda2): """ The effective lambda parameter The mass-weighted dominant effective lambda parameter defined in https://journals.aps.org/prd/pdf/10.1103/PhysRevD.91.043002 """ m1, m2, lambda1, lambda2, input_is_array = ensurearray( mass1, mass2, lambda1, lambda2) lsum = lambda1 + lambda2 ldiff, _ = ensurearray(lambda1 - lambda2) mask = m1 < m2 ldiff[mask] = -ldiff[mask] eta = eta_from_mass1_mass2(m1, m2) p1 = lsum * (1 + 7. * eta - 31 * eta ** 2.0) p2 = (1 - 4 * eta)**0.5 * (1 + 9 * eta - 11 * eta ** 2.0) * ldiff return formatreturn(8.0 / 13.0 * (p1 + p2), input_is_array)
[docs] def delta_lambda_tilde(mass1, mass2, lambda1, lambda2): """ Delta lambda tilde parameter defined as equation 15 in https://journals.aps.org/prd/pdf/10.1103/PhysRevD.91.043002 """ m1, m2, lambda1, lambda2, input_is_array = ensurearray( mass1, mass2, lambda1, lambda2) lsum = lambda1 + lambda2 ldiff, _ = ensurearray(lambda1 - lambda2) mask = m1 < m2 ldiff[mask] = -ldiff[mask] eta = eta_from_mass1_mass2(m1, m2) p1 = numpy.sqrt(1 - 4 * eta) * ( 1 - (13272 / 1319) * eta + (8944 / 1319) * eta ** 2 ) * lsum p2 = ( 1 - (15910 / 1319) * eta + (32850 / 1319) * eta ** 2 + (3380 / 1319) * eta ** 3 ) * ldiff return formatreturn(1 / 2 * (p1 + p2), input_is_array)
[docs] def lambda1_from_delta_lambda_tilde_lambda_tilde(delta_lambda_tilde, lambda_tilde, mass1, mass2): """ Returns lambda1 parameter by using delta lambda tilde, lambda tilde, mass1, and mass2. """ m1, m2, delta_lambda_tilde, lambda_tilde, input_is_array = ensurearray( mass1, mass2, delta_lambda_tilde, lambda_tilde) eta = eta_from_mass1_mass2(m1, m2) p1 = 1 + 7.0*eta - 31*eta**2.0 p2 = (1 - 4*eta)**0.5 * (1 + 9*eta - 11*eta**2.0) p3 = (1 - 4*eta)**0.5 * (1 - 13272/1319*eta + 8944/1319*eta**2) p4 = 1 - (15910/1319)*eta + (32850/1319)*eta**2 + (3380/1319)*eta**3 amp = 1/((p1*p4)-(p2*p3)) l_tilde_lambda1 = 13/16 * (p3-p4) * lambda_tilde l_delta_tilde_lambda1 = (p1-p2) * delta_lambda_tilde lambda1 = formatreturn( amp * (l_delta_tilde_lambda1 - l_tilde_lambda1), input_is_array ) return lambda1
[docs] def lambda2_from_delta_lambda_tilde_lambda_tilde( delta_lambda_tilde, lambda_tilde, mass1, mass2): """ Returns lambda2 parameter by using delta lambda tilde, lambda tilde, mass1, and mass2. """ m1, m2, delta_lambda_tilde, lambda_tilde, input_is_array = ensurearray( mass1, mass2, delta_lambda_tilde, lambda_tilde) eta = eta_from_mass1_mass2(m1, m2) p1 = 1 + 7.0*eta - 31*eta**2.0 p2 = (1 - 4*eta)**0.5 * (1 + 9*eta - 11*eta**2.0) p3 = (1 - 4*eta)**0.5 * (1 - 13272/1319*eta + 8944/1319*eta**2) p4 = 1 - (15910/1319)*eta + (32850/1319)*eta**2 + (3380/1319)*eta**3 amp = 1/((p1*p4)-(p2*p3)) l_tilde_lambda2 = 13/16 * (p3+p4) * lambda_tilde l_delta_tilde_lambda2 = (p1+p2) * delta_lambda_tilde lambda2 = formatreturn( amp * (l_tilde_lambda2 - l_delta_tilde_lambda2), input_is_array ) return lambda2
[docs] def lambda_from_mass_tov_file(mass, tov_file, distance=0.): """Return the lambda parameter(s) corresponding to the input mass(es) interpolating from the mass-Lambda data for a particular EOS read in from an ASCII file. """ data = numpy.loadtxt(tov_file) mass_from_file = data[:, 0] lambda_from_file = data[:, 1] mass_src = mass/(1.0 + pycbc.cosmology.redshift(distance)) lambdav = numpy.interp(mass_src, mass_from_file, lambda_from_file) return lambdav
def ensure_obj1_is_primary(mass1, mass2, *params): """ Enforce that the object labelled as 1 is the primary. Parameters ---------- mass1 : float, numpy.array Mass values labelled as 1. mass2 : float, numpy.array Mass values labelled as 2. *params : The binary parameters to be swapped around when mass1 < mass2. The list must have length 2N and it must be organized so that params[i] and params[i+1] are the same kind of quantity, but for object 1 and object 2, respsectively. E.g., spin1z, spin2z, lambda1, lambda2. Returns ------- list : A list with mass1, mass2, params as arrays, with elements, each with elements re-arranged so that object 1 is the primary. """ # Check params are 2N if len(params) % 2 != 0: raise ValueError("params must be 2N floats or arrays") input_properties, input_is_array = ensurearray((mass1, mass2)+params) # Check inputs are all the same length shapes = [par.shape for par in input_properties] if len(set(shapes)) != 1: raise ValueError("Individual masses and params must have same shape") # What needs to be swapped mask = mass1 < mass2 # Output containter output_properties = [] for i in numpy.arange(0, len(shapes), 2): # primary (p) p = copy.copy(input_properties[i]) # secondary (s) s = copy.copy(input_properties[i+1]) # Swap p[mask] = input_properties[i+1][mask] s[mask] = input_properties[i][mask] # Format and include in output object output_properties.append(formatreturn(p, input_is_array)) output_properties.append(formatreturn(s, input_is_array)) # Release output return output_properties
[docs] def remnant_mass_from_mass1_mass2_spherical_spin_eos( mass1, mass2, spin1_a=0.0, spin1_polar=0.0, eos='2H', spin2_a=0.0, spin2_polar=0.0, swap_companions=False, ns_bh_mass_boundary=None, extrapolate=False): """ Function that determines the remnant disk mass of an NS-BH system using the fit to numerical-relativity results discussed in Foucart, Hinderer & Nissanke, PRD 98, 081501(R) (2018). The BH spin may be misaligned with the orbital angular momentum. In such cases the ISSO is approximated following the approach of Stone, Loeb & Berger, PRD 87, 084053 (2013), which was originally devised for a previous NS-BH remnant mass fit of Foucart, PRD 86, 124007 (2012). Note: The NS spin does not play any role in this fit! Parameters ----------- mass1 : float The mass of the black hole, in solar masses. mass2 : float The mass of the neutron star, in solar masses. spin1_a : float, optional The dimensionless magnitude of the spin of mass1. Default = 0. spin1_polar : float, optional The tilt angle of the spin of mass1. Default = 0 (aligned w L). eos : str, optional Name of the equation of state being adopted. Default is '2H'. spin2_a : float, optional The dimensionless magnitude of the spin of mass2. Default = 0. spin2_polar : float, optional The tilt angle of the spin of mass2. Default = 0 (aligned w L). swap_companions : boolean, optional If mass2 > mass1, swap mass and spin of object 1 and 2 prior to applying the fitting formula (otherwise fail). Default is False. ns_bh_mass_boundary : float, optional If mass2 is greater than this value, the neutron star is effectively treated as a black hole and the returned value is 0. For consistency with the eos, set this to the maximum mass allowed by the eos; set a lower value for a more stringent cut. Default is None. extrapolate : boolean, optional Invoke extrapolation of NS baryonic mass and NS compactness in scipy.interpolate.interp1d at low masses. If ns_bh_mass_boundary is provided, it is applied at high masses, otherwise the equation of state prescribes the maximum possible mass2. Default is False. Returns ---------- remnant_mass: float The remnant mass in solar masses """ mass1, mass2, spin1_a, spin1_polar, spin2_a, spin2_polar, \ input_is_array = \ ensurearray(mass1, mass2, spin1_a, spin1_polar, spin2_a, spin2_polar) # mass1 must be greater than mass2: swap the properties of 1 and 2 or fail if swap_companions: mass1, mass2, spin1_a, spin2_a, spin1_polar, spin2_polar = \ ensure_obj1_is_primary(mass1, mass2, spin1_a, spin2_a, spin1_polar, spin2_polar) else: try: if any(mass2 > mass1) and input_is_array: raise ValueError(f'Require mass1 >= mass2') except TypeError: if mass2 > mass1 and not input_is_array: raise ValueError(f'Require mass1 >= mass2. {mass1} < {mass2}') eta = eta_from_mass1_mass2(mass1, mass2) # If a maximum NS mass is not provided, accept all values and # let the EOS handle this (in ns.initialize_eos) if ns_bh_mass_boundary is None: mask = numpy.ones(ensurearray(mass2)[0].size, dtype=bool) # Otherwise perform the calculation only for small enough NS masses... else: mask = mass2 <= ns_bh_mass_boundary # ...and return 0's otherwise remnant_mass = numpy.zeros(ensurearray(mass2)[0].size) ns_compactness, ns_b_mass = ns.initialize_eos(mass2[mask], eos, extrapolate=extrapolate) remnant_mass[mask] = ns.foucart18( eta[mask], ns_compactness, ns_b_mass, spin1_a[mask], spin1_polar[mask]) return formatreturn(remnant_mass, input_is_array)
[docs] def remnant_mass_from_mass1_mass2_cartesian_spin_eos( mass1, mass2, spin1x=0.0, spin1y=0.0, spin1z=0.0, eos='2H', spin2x=0.0, spin2y=0.0, spin2z=0.0, swap_companions=False, ns_bh_mass_boundary=None, extrapolate=False): """ Function that determines the remnant disk mass of an NS-BH system using the fit to numerical-relativity results discussed in Foucart, Hinderer & Nissanke, PRD 98, 081501(R) (2018). The BH spin may be misaligned with the orbital angular momentum. In such cases the ISSO is approximated following the approach of Stone, Loeb & Berger, PRD 87, 084053 (2013), which was originally devised for a previous NS-BH remnant mass fit of Foucart, PRD 86, 124007 (2012). Note: NS spin is assumed to be 0! Parameters ----------- mass1 : float The mass of the black hole, in solar masses. mass2 : float The mass of the neutron star, in solar masses. spin1x : float, optional The dimensionless x-component of the spin of mass1. Default = 0. spin1y : float, optional The dimensionless y-component of the spin of mass1. Default = 0. spin1z : float, optional The dimensionless z-component of the spin of mass1. Default = 0. eos: str, optional Name of the equation of state being adopted. Default is '2H'. spin2x : float, optional The dimensionless x-component of the spin of mass2. Default = 0. spin2y : float, optional The dimensionless y-component of the spin of mass2. Default = 0. spin2z : float, optional The dimensionless z-component of the spin of mass2. Default = 0. swap_companions : boolean, optional If mass2 > mass1, swap mass and spin of object 1 and 2 prior to applying the fitting formula (otherwise fail). Default is False. ns_bh_mass_boundary : float, optional If mass2 is greater than this value, the neutron star is effectively treated as a black hole and the returned value is 0. For consistency with the eos, set this to the maximum mass allowed by the eos; set a lower value for a more stringent cut. Default is None. extrapolate : boolean, optional Invoke extrapolation of NS baryonic mass and NS compactness in scipy.interpolate.interp1d at low masses. If ns_bh_mass_boundary is provided, it is applied at high masses, otherwise the equation of state prescribes the maximum possible mass2. Default is False. Returns ---------- remnant_mass: float The remnant mass in solar masses """ spin1_a, _, spin1_polar = _cartesian_to_spherical(spin1x, spin1y, spin1z) if swap_companions: spin2_a, _, spin2_polar = _cartesian_to_spherical(spin2x, spin2y, spin2z) else: size = ensurearray(spin1_a)[0].size spin2_a = numpy.zeros(size) spin2_polar = numpy.zeros(size) return remnant_mass_from_mass1_mass2_spherical_spin_eos( mass1, mass2, spin1_a=spin1_a, spin1_polar=spin1_polar, eos=eos, spin2_a=spin2_a, spin2_polar=spin2_polar, swap_companions=swap_companions, ns_bh_mass_boundary=ns_bh_mass_boundary, extrapolate=extrapolate)
# # ============================================================================= # # CBC spin functions # # ============================================================================= #
[docs] def chi_eff(mass1, mass2, spin1z, spin2z): """Returns the effective spin from mass1, mass2, spin1z, and spin2z.""" return (spin1z * mass1 + spin2z * mass2) / (mass1 + mass2)
[docs] def chi_a(mass1, mass2, spin1z, spin2z): """ Returns the aligned mass-weighted spin difference from mass1, mass2, spin1z, and spin2z. """ return (spin2z * mass2 - spin1z * mass1) / (mass2 + mass1)
[docs] def chi_p(mass1, mass2, spin1x, spin1y, spin2x, spin2y): """Returns the effective precession spin from mass1, mass2, spin1x, spin1y, spin2x, and spin2y. """ xi1 = secondary_xi(mass1, mass2, spin1x, spin1y, spin2x, spin2y) xi2 = primary_xi(mass1, mass2, spin1x, spin1y, spin2x, spin2y) return chi_p_from_xi1_xi2(xi1, xi2)
[docs] def phi_a(mass1, mass2, spin1x, spin1y, spin2x, spin2y): """ Returns the angle between the in-plane perpendicular spins.""" phi1 = phi_from_spinx_spiny(primary_spin(mass1, mass2, spin1x, spin2x), primary_spin(mass1, mass2, spin1y, spin2y)) phi2 = phi_from_spinx_spiny(secondary_spin(mass1, mass2, spin1x, spin2x), secondary_spin(mass1, mass2, spin1y, spin2y)) return (phi1 - phi2) % (2 * numpy.pi)
[docs] def phi_s(spin1x, spin1y, spin2x, spin2y): """ Returns the sum of the in-plane perpendicular spins.""" phi1 = phi_from_spinx_spiny(spin1x, spin1y) phi2 = phi_from_spinx_spiny(spin2x, spin2y) return (phi1 + phi2) % (2 * numpy.pi)
[docs] def chi_eff_from_spherical(mass1, mass2, spin1_a, spin1_polar, spin2_a, spin2_polar): """Returns the effective spin using spins in spherical coordinates.""" spin1z = spin1_a * numpy.cos(spin1_polar) spin2z = spin2_a * numpy.cos(spin2_polar) return chi_eff(mass1, mass2, spin1z, spin2z)
[docs] def chi_p_from_spherical(mass1, mass2, spin1_a, spin1_azimuthal, spin1_polar, spin2_a, spin2_azimuthal, spin2_polar): """Returns the effective precession spin using spins in spherical coordinates. """ spin1x, spin1y, _ = _spherical_to_cartesian( spin1_a, spin1_azimuthal, spin1_polar) spin2x, spin2y, _ = _spherical_to_cartesian( spin2_a, spin2_azimuthal, spin2_polar) return chi_p(mass1, mass2, spin1x, spin1y, spin2x, spin2y)
[docs] def primary_spin(mass1, mass2, spin1, spin2): """Returns the dimensionless spin of the primary mass.""" mass1, mass2, spin1, spin2, input_is_array = ensurearray( mass1, mass2, spin1, spin2) sp = copy.copy(spin1) mask = mass1 < mass2 sp[mask] = spin2[mask] return formatreturn(sp, input_is_array)
[docs] def secondary_spin(mass1, mass2, spin1, spin2): """Returns the dimensionless spin of the secondary mass.""" mass1, mass2, spin1, spin2, input_is_array = ensurearray( mass1, mass2, spin1, spin2) ss = copy.copy(spin2) mask = mass1 < mass2 ss[mask] = spin1[mask] return formatreturn(ss, input_is_array)
[docs] def primary_xi(mass1, mass2, spin1x, spin1y, spin2x, spin2y): """Returns the effective precession spin argument for the larger mass. """ spinx = primary_spin(mass1, mass2, spin1x, spin2x) spiny = primary_spin(mass1, mass2, spin1y, spin2y) return chi_perp_from_spinx_spiny(spinx, spiny)
[docs] def secondary_xi(mass1, mass2, spin1x, spin1y, spin2x, spin2y): """Returns the effective precession spin argument for the smaller mass. """ spinx = secondary_spin(mass1, mass2, spin1x, spin2x) spiny = secondary_spin(mass1, mass2, spin1y, spin2y) return xi2_from_mass1_mass2_spin2x_spin2y(mass1, mass2, spinx, spiny)
[docs] def xi1_from_spin1x_spin1y(spin1x, spin1y): """Returns the effective precession spin argument for the larger mass. This function assumes it's given spins of the primary mass. """ return chi_perp_from_spinx_spiny(spin1x, spin1y)
[docs] def xi2_from_mass1_mass2_spin2x_spin2y(mass1, mass2, spin2x, spin2y): """Returns the effective precession spin argument for the smaller mass. This function assumes it's given spins of the secondary mass. """ q = q_from_mass1_mass2(mass1, mass2) a1 = 2 + 3 * q / 2 a2 = 2 + 3 / (2 * q) return a1 / (q**2 * a2) * chi_perp_from_spinx_spiny(spin2x, spin2y)
[docs] def chi_perp_from_spinx_spiny(spinx, spiny): """Returns the in-plane spin from the x/y components of the spin. """ return numpy.sqrt(spinx**2 + spiny**2)
[docs] def chi_perp_from_mass1_mass2_xi2(mass1, mass2, xi2): """Returns the in-plane spin from mass1, mass2, and xi2 for the secondary mass. """ q = q_from_mass1_mass2(mass1, mass2) a1 = 2 + 3 * q / 2 a2 = 2 + 3 / (2 * q) return q**2 * a2 / a1 * xi2
[docs] def chi_p_from_xi1_xi2(xi1, xi2): """Returns effective precession spin from xi1 and xi2. """ xi1, xi2, input_is_array = ensurearray(xi1, xi2) chi_p = copy.copy(xi1) mask = xi1 < xi2 chi_p[mask] = xi2[mask] return formatreturn(chi_p, input_is_array)
[docs] def phi1_from_phi_a_phi_s(phi_a, phi_s): """Returns the angle between the x-component axis and the in-plane spin for the primary mass from phi_s and phi_a. """ return (phi_s + phi_a) / 2.0
[docs] def phi2_from_phi_a_phi_s(phi_a, phi_s): """Returns the angle between the x-component axis and the in-plane spin for the secondary mass from phi_s and phi_a. """ return (phi_s - phi_a) / 2.0
[docs] def phi_from_spinx_spiny(spinx, spiny): """Returns the angle between the x-component axis and the in-plane spin. """ phi = numpy.arctan2(spiny, spinx) return phi % (2 * numpy.pi)
[docs] def spin1z_from_mass1_mass2_chi_eff_chi_a(mass1, mass2, chi_eff, chi_a): """Returns spin1z. """ return (mass1 + mass2) / (2.0 * mass1) * (chi_eff - chi_a)
[docs] def spin2z_from_mass1_mass2_chi_eff_chi_a(mass1, mass2, chi_eff, chi_a): """Returns spin2z. """ return (mass1 + mass2) / (2.0 * mass2) * (chi_eff + chi_a)
[docs] def spin1x_from_xi1_phi_a_phi_s(xi1, phi_a, phi_s): """Returns x-component spin for primary mass. """ phi1 = phi1_from_phi_a_phi_s(phi_a, phi_s) return xi1 * numpy.cos(phi1)
[docs] def spin1y_from_xi1_phi_a_phi_s(xi1, phi_a, phi_s): """Returns y-component spin for primary mass. """ phi1 = phi1_from_phi_a_phi_s(phi_s, phi_a) return xi1 * numpy.sin(phi1)
[docs] def spin2x_from_mass1_mass2_xi2_phi_a_phi_s(mass1, mass2, xi2, phi_a, phi_s): """Returns x-component spin for secondary mass. """ chi_perp = chi_perp_from_mass1_mass2_xi2(mass1, mass2, xi2) phi2 = phi2_from_phi_a_phi_s(phi_a, phi_s) return chi_perp * numpy.cos(phi2)
[docs] def spin2y_from_mass1_mass2_xi2_phi_a_phi_s(mass1, mass2, xi2, phi_a, phi_s): """Returns y-component spin for secondary mass. """ chi_perp = chi_perp_from_mass1_mass2_xi2(mass1, mass2, xi2) phi2 = phi2_from_phi_a_phi_s(phi_a, phi_s) return chi_perp * numpy.sin(phi2)
[docs] def dquadmon_from_lambda(lambdav): r"""Return the quadrupole moment of a neutron star given its lambda We use the relations defined here. https://arxiv.org/pdf/1302.4499.pdf. Note that the convention we use is that: .. math:: \mathrm{dquadmon} = \bar{Q} - 1. Where :math:`\bar{Q}` (dimensionless) is the reduced quadrupole moment. """ ll = numpy.log(lambdav) ai = .194 bi = .0936 ci = 0.0474 di = -4.21 * 10**-3.0 ei = 1.23 * 10**-4.0 ln_quad_moment = ai + bi*ll + ci*ll**2.0 + di*ll**3.0 + ei*ll**4.0 return numpy.exp(ln_quad_moment) - 1
[docs] def spin_from_pulsar_freq(mass, radius, freq): """Returns the dimensionless spin of a pulsar. Assumes the pulsar is a solid sphere when computing the moment of inertia. Parameters ---------- mass : float The mass of the pulsar, in solar masses. radius : float The assumed radius of the pulsar, in kilometers. freq : float The spin frequency of the pulsar, in Hz. """ omega = 2 * numpy.pi * freq mt = mass * lal.MTSUN_SI mominert = (2/5.) * mt * (radius * 1000 / lal.C_SI)**2 return mominert * omega / mt**2
# # ============================================================================= # # Extrinsic parameter functions # # ============================================================================= #
[docs] def chirp_distance(dist, mchirp, ref_mass=1.4): """Returns the chirp distance given the luminosity distance and chirp mass. """ return dist * (2.**(-1./5) * ref_mass / mchirp)**(5./6)
def distance_from_chirp_distance_mchirp(chirp_distance, mchirp, ref_mass=1.4): """Returns the luminosity distance given a chirp distance and chirp mass. """ return chirp_distance * (2.**(-1./5) * ref_mass / mchirp)**(-5./6) _detector_cache = {}
[docs] def det_tc(detector_name, ra, dec, tc, ref_frame='geocentric', relative=False): """Returns the coalescence time of a signal in the given detector. Parameters ---------- detector_name : string The name of the detector, e.g., 'H1'. ra : float The right ascension of the signal, in radians. dec : float The declination of the signal, in radians. tc : float The GPS time of the coalescence of the signal in the `ref_frame`. ref_frame : {'geocentric', string} The reference frame that the given coalescence time is defined in. May specify 'geocentric', or a detector name; default is 'geocentric'. Returns ------- float : The GPS time of the coalescence in detector `detector_name`. """ ref_time = tc if relative: tc = 0 if ref_frame == detector_name: return tc if detector_name not in _detector_cache: _detector_cache[detector_name] = Detector(detector_name) detector = _detector_cache[detector_name] if ref_frame == 'geocentric': return tc + detector.time_delay_from_earth_center(ra, dec, ref_time) else: other = Detector(ref_frame) return tc + detector.time_delay_from_detector(other, ra, dec, ref_time)
def optimal_orientation_from_detector(detector_name, tc): """ Low-level function to be called from _optimal_dec_from_detector and _optimal_ra_from_detector""" d = Detector(detector_name) ra, dec = d.optimal_orientation(tc) return ra, dec
[docs] def optimal_dec_from_detector(detector_name, tc): """For a given detector and GPS time, return the optimal orientation (directly overhead of the detector) in declination. Parameters ---------- detector_name : string The name of the detector, e.g., 'H1'. tc : float The GPS time of the coalescence of the signal in the `ref_frame`. Returns ------- float : The declination of the signal, in radians. """ return optimal_orientation_from_detector(detector_name, tc)[1]
[docs] def optimal_ra_from_detector(detector_name, tc): """For a given detector and GPS time, return the optimal orientation (directly overhead of the detector) in right ascension. Parameters ---------- detector_name : string The name of the detector, e.g., 'H1'. tc : float The GPS time of the coalescence of the signal in the `ref_frame`. Returns ------- float : The declination of the signal, in radians. """ return optimal_orientation_from_detector(detector_name, tc)[0]
# # ============================================================================= # # Likelihood statistic parameter functions # # ============================================================================= #
[docs] def snr_from_loglr(loglr): """Returns SNR computed from the given log likelihood ratio(s). This is defined as `sqrt(2*loglr)`.If the log likelihood ratio is < 0, returns 0. Parameters ---------- loglr : array or float The log likelihood ratio(s) to evaluate. Returns ------- array or float The SNRs computed from the log likelihood ratios. """ singleval = isinstance(loglr, float) if singleval: loglr = numpy.array([loglr]) # temporarily quiet sqrt(-1) warnings with numpy.errstate(invalid="ignore"): snrs = numpy.sqrt(2*loglr) snrs[numpy.isnan(snrs)] = 0. if singleval: snrs = snrs[0] return snrs
# # ============================================================================= # # BH Ringdown functions # # ============================================================================= # def get_lm_f0tau(mass, spin, l, m, n=0, which='both'): """Return the f0 and the tau for one or more overtones of an l, m mode. Parameters ---------- mass : float or array Mass of the black hole (in solar masses). spin : float or array Dimensionless spin of the final black hole. l : int or array l-index of the harmonic. m : int or array m-index of the harmonic. n : int or array Overtone(s) to generate, where n=0 is the fundamental mode. Default is 0. which : {'both', 'f0', 'tau'}, optional What to return; 'both' returns both frequency and tau, 'f0' just frequency, 'tau' just tau. Default is 'both'. Returns ------- f0 : float or array Returned if ``which`` is 'both' or 'f0'. The frequency of the QNM(s), in Hz. tau : float or array Returned if ``which`` is 'both' or 'tau'. The damping time of the QNM(s), in seconds. """ # convert to arrays mass, spin, l, m, n, input_is_array = ensurearray( mass, spin, l, m, n) # we'll ravel the arrays so we can evaluate each parameter combination # one at a a time getf0 = which == 'both' or which == 'f0' gettau = which == 'both' or which == 'tau' out = [] if getf0: f0s = pykerr.qnmfreq(mass, spin, l, m, n) out.append(formatreturn(f0s, input_is_array)) if gettau: taus = pykerr.qnmtau(mass, spin, l, m, n) out.append(formatreturn(taus, input_is_array)) if not (getf0 and gettau): out = out[0] return out def get_lm_f0tau_allmodes(mass, spin, modes): """Returns a dictionary of all of the frequencies and damping times for the requested modes. Parameters ---------- mass : float or array Mass of the black hole (in solar masses). spin : float or array Dimensionless spin of the final black hole. modes : list of str The modes to get. Each string in the list should be formatted 'lmN', where l (m) is the l (m) index of the harmonic and N is the number of overtones to generate (note, N is not the index of the overtone). Returns ------- f0 : dict Dictionary mapping the modes to the frequencies. The dictionary keys are 'lmn' string, where l (m) is the l (m) index of the harmonic and n is the index of the overtone. For example, '220' is the l = m = 2 mode and the 0th overtone. tau : dict Dictionary mapping the modes to the damping times. The keys are the same as ``f0``. """ f0, tau = {}, {} for lmn in modes: key = '{}{}{}' l, m, nmodes = int(lmn[0]), int(lmn[1]), int(lmn[2]) for n in range(nmodes): tmp_f0, tmp_tau = get_lm_f0tau(mass, spin, l, m, n) f0[key.format(l, abs(m), n)] = tmp_f0 tau[key.format(l, abs(m), n)] = tmp_tau return f0, tau
[docs] def freq_from_final_mass_spin(final_mass, final_spin, l=2, m=2, n=0): """Returns QNM frequency for the given mass and spin and mode. Parameters ---------- final_mass : float or array Mass of the black hole (in solar masses). final_spin : float or array Dimensionless spin of the final black hole. l : int or array, optional l-index of the harmonic. Default is 2. m : int or array, optional m-index of the harmonic. Default is 2. n : int or array Overtone(s) to generate, where n=0 is the fundamental mode. Default is 0. Returns ------- float or array The frequency of the QNM(s), in Hz. """ return get_lm_f0tau(final_mass, final_spin, l, m, n=n, which='f0')
[docs] def tau_from_final_mass_spin(final_mass, final_spin, l=2, m=2, n=0): """Returns QNM damping time for the given mass and spin and mode. Parameters ---------- final_mass : float or array Mass of the black hole (in solar masses). final_spin : float or array Dimensionless spin of the final black hole. l : int or array, optional l-index of the harmonic. Default is 2. m : int or array, optional m-index of the harmonic. Default is 2. n : int or array Overtone(s) to generate, where n=0 is the fundamental mode. Default is 0. Returns ------- float or array The damping time of the QNM(s), in seconds. """ return get_lm_f0tau(final_mass, final_spin, l, m, n=n, which='tau')
# The following are from Table VIII, IX, X of Berti et al., # PRD 73 064030, arXiv:gr-qc/0512160 (2006). # Keys are l,m (only n=0 supported). Constants are for converting from # frequency and damping time to mass and spin. _berti_spin_constants = { (2, 2): (0.7, 1.4187, -0.4990), (2, 1): (-0.3, 2.3561, -0.2277), (3, 3): (0.9, 2.343, -0.4810), (4, 4): (1.1929, 3.1191, -0.4825), } _berti_mass_constants = { (2, 2): (1.5251, -1.1568, 0.1292), (2, 1): (0.6, -0.2339, 0.4175), (3, 3): (1.8956, -1.3043, 0.1818), (4, 4): (2.3, -1.5056, 0.2244), }
[docs] def final_spin_from_f0_tau(f0, tau, l=2, m=2): """Returns the final spin based on the given frequency and damping time. .. note:: Currently, only (l,m) = (2,2), (3,3), (4,4), (2,1) are supported. Any other indices will raise a ``KeyError``. Parameters ---------- f0 : float or array Frequency of the QNM (in Hz). tau : float or array Damping time of the QNM (in seconds). l : int, optional l-index of the harmonic. Default is 2. m : int, optional m-index of the harmonic. Default is 2. Returns ------- float or array The spin of the final black hole. If the combination of frequency and damping times give an unphysical result, ``numpy.nan`` will be returned. """ f0, tau, input_is_array = ensurearray(f0, tau) # from Berti et al. 2006 a, b, c = _berti_spin_constants[l,m] origshape = f0.shape # flatten inputs for storing results f0 = f0.ravel() tau = tau.ravel() spins = numpy.zeros(f0.size) for ii in range(spins.size): Q = f0[ii] * tau[ii] * numpy.pi try: s = 1. - ((Q-a)/b)**(1./c) except ValueError: s = numpy.nan spins[ii] = s spins = spins.reshape(origshape) return formatreturn(spins, input_is_array)
[docs] def final_mass_from_f0_tau(f0, tau, l=2, m=2): """Returns the final mass (in solar masses) based on the given frequency and damping time. .. note:: Currently, only (l,m) = (2,2), (3,3), (4,4), (2,1) are supported. Any other indices will raise a ``KeyError``. Parameters ---------- f0 : float or array Frequency of the QNM (in Hz). tau : float or array Damping time of the QNM (in seconds). l : int, optional l-index of the harmonic. Default is 2. m : int, optional m-index of the harmonic. Default is 2. Returns ------- float or array The mass of the final black hole. If the combination of frequency and damping times give an unphysical result, ``numpy.nan`` will be returned. """ # from Berti et al. 2006 spin = final_spin_from_f0_tau(f0, tau, l=l, m=m) a, b, c = _berti_mass_constants[l,m] return (a + b*(1-spin)**c)/(2*numpy.pi*f0*lal.MTSUN_SI)
[docs] def freqlmn_from_other_lmn(f0, tau, current_l, current_m, new_l, new_m): """Returns the QNM frequency (in Hz) of a chosen new (l,m) mode from the given current (l,m) mode. Parameters ---------- f0 : float or array Frequency of the current QNM (in Hz). tau : float or array Damping time of the current QNM (in seconds). current_l : int, optional l-index of the current QNM. current_m : int, optional m-index of the current QNM. new_l : int, optional l-index of the new QNM to convert to. new_m : int, optional m-index of the new QNM to convert to. Returns ------- float or array The frequency of the new (l, m) QNM mode. If the combination of frequency and damping time provided for the current (l, m) QNM mode correspond to an unphysical Kerr black hole mass and/or spin, ``numpy.nan`` will be returned. """ mass = final_mass_from_f0_tau(f0, tau, l=current_l, m=current_m) spin = final_spin_from_f0_tau(f0, tau, l=current_l, m=current_m) mass, spin, input_is_array = ensurearray(mass, spin) mass[mass < 0] = numpy.nan spin[numpy.abs(spin) > 0.9996] = numpy.nan new_f0 = freq_from_final_mass_spin(mass, spin, l=new_l, m=new_m) return formatreturn(new_f0, input_is_array)
[docs] def taulmn_from_other_lmn(f0, tau, current_l, current_m, new_l, new_m): """Returns the QNM damping time (in seconds) of a chosen new (l,m) mode from the given current (l,m) mode. Parameters ---------- f0 : float or array Frequency of the current QNM (in Hz). tau : float or array Damping time of the current QNM (in seconds). current_l : int, optional l-index of the current QNM. current_m : int, optional m-index of the current QNM. new_l : int, optional l-index of the new QNM to convert to. new_m : int, optional m-index of the new QNM to convert to. Returns ------- float or array The daming time of the new (l, m) QNM mode. If the combination of frequency and damping time provided for the current (l, m) QNM mode correspond to an unphysical Kerr black hole mass and/or spin, ``numpy.nan`` will be returned. """ mass = final_mass_from_f0_tau(f0, tau, l=current_l, m=current_m) spin = final_spin_from_f0_tau(f0, tau, l=current_l, m=current_m) mass, spin, input_is_array = ensurearray(mass, spin) mass[mass < 0] = numpy.nan spin[numpy.abs(spin) > 0.9996] = numpy.nan new_tau = tau_from_final_mass_spin(mass, spin, l=new_l, m=new_m) return formatreturn(new_tau, input_is_array)
def get_final_from_initial(mass1, mass2, spin1x=0., spin1y=0., spin1z=0., spin2x=0., spin2y=0., spin2z=0., approximant='SEOBNRv4PHM', f_ref=-1): """Estimates the final mass and spin from the given initial parameters. This uses the fits used by either the NRSur7dq4 or EOBNR models for converting from initial parameters to final, depending on the ``approximant`` argument. Parameters ---------- mass1 : float The mass of one of the components, in solar masses. mass2 : float The mass of the other component, in solar masses. spin1x : float, optional The dimensionless x-component of the spin of mass1. Default is 0. spin1y : float, optional The dimensionless y-component of the spin of mass1. Default is 0. spin1z : float, optional The dimensionless z-component of the spin of mass1. Default is 0. spin2x : float, optional The dimensionless x-component of the spin of mass2. Default is 0. spin2y : float, optional The dimensionless y-component of the spin of mass2. Default is 0. spin2z : float, optional The dimensionless z-component of the spin of mass2. Default is 0. approximant : str, optional The waveform approximant to use for the fit function. If "NRSur7dq4", the NRSur7dq4Remnant fit in lalsimulation will be used. If "SEOBNRv4", the ``XLALSimIMREOBFinalMassSpin`` function in lalsimulation will be used. Otherwise, ``XLALSimIMREOBFinalMassSpinPrec`` from lalsimulation will be used, with the approximant name passed as the approximant in that function ("SEOBNRv4PHM" will work with this function). Default is "SEOBNRv4PHM". f_ref : float, optional The reference frequency for the spins. Only used by the NRSur7dq4 fit. Default (-1) will use the default reference frequency for the approximant. Returns ------- final_mass : float The final mass, in solar masses. final_spin : float The dimensionless final spin. """ args = (mass1, mass2, spin1x, spin1y, spin1z, spin2x, spin2y, spin2z) args = ensurearray(*args) input_is_array = args[-1] origshape = args[0].shape # flatten inputs for storing results args = [a.ravel() for a in args[:-1]] mass1, mass2, spin1x, spin1y, spin1z, spin2x, spin2y, spin2z = args final_mass = numpy.full(mass1.shape, numpy.nan) final_spin = numpy.full(mass1.shape, numpy.nan) for ii in range(final_mass.size): m1 = float(mass1[ii]) m2 = float(mass2[ii]) spin1 = list(map(float, [spin1x[ii], spin1y[ii], spin1z[ii]])) spin2 = list(map(float, [spin2x[ii], spin2y[ii], spin2z[ii]])) if approximant == 'NRSur7dq4': from lalsimulation import nrfits try: res = nrfits.eval_nrfit(m1*lal.MSUN_SI, m2*lal.MSUN_SI, spin1, spin2, 'NRSur7dq4Remnant', ['FinalMass', 'FinalSpin'], f_ref=f_ref) except RuntimeError: continue final_mass[ii] = res['FinalMass'][0] / lal.MSUN_SI sf = res['FinalSpin'] final_spin[ii] = (sf**2).sum()**0.5 if sf[-1] < 0: final_spin[ii] *= -1 elif approximant == 'SEOBNRv4': _, fm, fs = lalsim.SimIMREOBFinalMassSpin( m1, m2, spin1, spin2, getattr(lalsim, approximant)) final_mass[ii] = fm * (m1 + m2) final_spin[ii] = fs else: _, fm, fs = lalsim.SimIMREOBFinalMassSpinPrec( m1, m2, spin1, spin2, getattr(lalsim, approximant)) final_mass[ii] = fm * (m1 + m2) final_spin[ii] = fs final_mass = final_mass.reshape(origshape) final_spin = final_spin.reshape(origshape) return (formatreturn(final_mass, input_is_array), formatreturn(final_spin, input_is_array))
[docs] def final_mass_from_initial(mass1, mass2, spin1x=0., spin1y=0., spin1z=0., spin2x=0., spin2y=0., spin2z=0., approximant='SEOBNRv4PHM', f_ref=-1): """Estimates the final mass from the given initial parameters. This uses the fits used by either the NRSur7dq4 or EOBNR models for converting from initial parameters to final, depending on the ``approximant`` argument. Parameters ---------- mass1 : float The mass of one of the components, in solar masses. mass2 : float The mass of the other component, in solar masses. spin1x : float, optional The dimensionless x-component of the spin of mass1. Default is 0. spin1y : float, optional The dimensionless y-component of the spin of mass1. Default is 0. spin1z : float, optional The dimensionless z-component of the spin of mass1. Default is 0. spin2x : float, optional The dimensionless x-component of the spin of mass2. Default is 0. spin2y : float, optional The dimensionless y-component of the spin of mass2. Default is 0. spin2z : float, optional The dimensionless z-component of the spin of mass2. Default is 0. approximant : str, optional The waveform approximant to use for the fit function. If "NRSur7dq4", the NRSur7dq4Remnant fit in lalsimulation will be used. If "SEOBNRv4", the ``XLALSimIMREOBFinalMassSpin`` function in lalsimulation will be used. Otherwise, ``XLALSimIMREOBFinalMassSpinPrec`` from lalsimulation will be used, with the approximant name passed as the approximant in that function ("SEOBNRv4PHM" will work with this function). Default is "SEOBNRv4PHM". f_ref : float, optional The reference frequency for the spins. Only used by the NRSur7dq4 fit. Default (-1) will use the default reference frequency for the approximant. Returns ------- float The final mass, in solar masses. """ return get_final_from_initial(mass1, mass2, spin1x, spin1y, spin1z, spin2x, spin2y, spin2z, approximant, f_ref=f_ref)[0]
[docs] def final_spin_from_initial(mass1, mass2, spin1x=0., spin1y=0., spin1z=0., spin2x=0., spin2y=0., spin2z=0., approximant='SEOBNRv4PHM', f_ref=-1): """Estimates the final spin from the given initial parameters. This uses the fits used by either the NRSur7dq4 or EOBNR models for converting from initial parameters to final, depending on the ``approximant`` argument. Parameters ---------- mass1 : float The mass of one of the components, in solar masses. mass2 : float The mass of the other component, in solar masses. spin1x : float, optional The dimensionless x-component of the spin of mass1. Default is 0. spin1y : float, optional The dimensionless y-component of the spin of mass1. Default is 0. spin1z : float, optional The dimensionless z-component of the spin of mass1. Default is 0. spin2x : float, optional The dimensionless x-component of the spin of mass2. Default is 0. spin2y : float, optional The dimensionless y-component of the spin of mass2. Default is 0. spin2z : float, optional The dimensionless z-component of the spin of mass2. Default is 0. approximant : str, optional The waveform approximant to use for the fit function. If "NRSur7dq4", the NRSur7dq4Remnant fit in lalsimulation will be used. If "SEOBNRv4", the ``XLALSimIMREOBFinalMassSpin`` function in lalsimulation will be used. Otherwise, ``XLALSimIMREOBFinalMassSpinPrec`` from lalsimulation will be used, with the approximant name passed as the approximant in that function ("SEOBNRv4PHM" will work with this function). Default is "SEOBNRv4PHM". f_ref : float, optional The reference frequency for the spins. Only used by the NRSur7dq4 fit. Default (-1) will use the default reference frequency for the approximant. Returns ------- float The dimensionless final spin. """ return get_final_from_initial(mass1, mass2, spin1x, spin1y, spin1z, spin2x, spin2y, spin2z, approximant, f_ref=f_ref)[1]
# # ============================================================================= # # post-Newtonian functions # # ============================================================================= # def velocity_to_frequency(v, M): """ Calculate the gravitational-wave frequency from the total mass and invariant velocity. Parameters ---------- v : float Invariant velocity M : float Binary total mass Returns ------- f : float Gravitational-wave frequency """ return v**(3.0) / (M * lal.MTSUN_SI * lal.PI) def frequency_to_velocity(f, M): """ Calculate the invariant velocity from the total mass and gravitational-wave frequency. Parameters ---------- f: float Gravitational-wave frequency M: float Binary total mass Returns ------- v : float or numpy.array Invariant velocity """ return (lal.PI * M * lal.MTSUN_SI * f)**(1.0/3.0) def f_schwarzchild_isco(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 velocity_to_frequency((1.0/6.0)**(0.5), M) # # ============================================================================ # # p-g mode non-linear tide functions # # ============================================================================ # def nltides_coefs(amplitude, n, m1, m2): """Calculate the coefficents needed to compute the shift in t(f) and phi(f) due to non-linear tides. Parameters ---------- amplitude: float Amplitude of effect n: float Growth dependence of effect m1: float Mass of component 1 m2: float Mass of component 2 Returns ------- f_ref : float Reference frequency used to define A and n t_of_f_factor: float The constant factor needed to compute t(f) phi_of_f_factor: float The constant factor needed to compute phi(f) """ # Use 100.0 Hz as a reference frequency f_ref = 100.0 # Calculate chirp mass mc = mchirp_from_mass1_mass2(m1, m2) mc *= lal.lal.MSUN_SI # Calculate constants in phasing a = (96./5.) * \ (lal.lal.G_SI * lal.lal.PI * mc * f_ref / lal.lal.C_SI**3.)**(5./3.) b = 6. * amplitude t_of_f_factor = -1./(lal.lal.PI*f_ref) * b/(a*a * (n-4.)) phi_of_f_factor = -2.*b / (a*a * (n-3.)) return f_ref, t_of_f_factor, phi_of_f_factor def nltides_gw_phase_difference(f, f0, amplitude, n, m1, m2): """Calculate the gravitational-wave phase shift bwtween f and f_coalescence = infinity due to non-linear tides. To compute the phase shift between e.g. f_low and f_isco, call this function twice and compute the difference. Parameters ---------- f: float or numpy.array Frequency from which to compute phase f0: float or numpy.array Frequency that NL effects switch on amplitude: float or numpy.array Amplitude of effect n: float or numpy.array Growth dependence of effect m1: float or numpy.array Mass of component 1 m2: float or numpy.array Mass of component 2 Returns ------- delta_phi: float or numpy.array Phase in radians """ f, f0, amplitude, n, m1, m2, input_is_array = ensurearray( f, f0, amplitude, n, m1, m2) delta_phi = numpy.zeros(m1.shape) f_ref, _, phi_of_f_factor = nltides_coefs(amplitude, n, m1, m2) mask = f <= f0 delta_phi[mask] = - phi_of_f_factor[mask] * (f0[mask]/f_ref)**(n[mask]-3.) mask = f > f0 delta_phi[mask] = - phi_of_f_factor[mask] * (f[mask]/f_ref)**(n[mask]-3.) return formatreturn(delta_phi, input_is_array)
[docs] def nltides_gw_phase_diff_isco(f_low, f0, amplitude, n, m1, m2): """Calculate the gravitational-wave phase shift bwtween f_low and f_isco due to non-linear tides. Parameters ---------- f_low: float Frequency from which to compute phase. If the other arguments are passed as numpy arrays then the value of f_low is duplicated for all elements in the array f0: float or numpy.array Frequency that NL effects switch on amplitude: float or numpy.array Amplitude of effect n: float or numpy.array Growth dependence of effect m1: float or numpy.array Mass of component 1 m2: float or numpy.array Mass of component 2 Returns ------- delta_phi: float or numpy.array Phase in radians """ f0, amplitude, n, m1, m2, input_is_array = ensurearray( f0, amplitude, n, m1, m2) f_low = numpy.zeros(m1.shape) + f_low phi_l = nltides_gw_phase_difference( f_low, f0, amplitude, n, m1, m2) f_isco = f_schwarzchild_isco(m1+m2) phi_i = nltides_gw_phase_difference( f_isco, f0, amplitude, n, m1, m2) return formatreturn(phi_i - phi_l, input_is_array)
__all__ = ['dquadmon_from_lambda', 'lambda_tilde', 'lambda_from_mass_tov_file', 'primary_mass', 'secondary_mass', 'mtotal_from_mass1_mass2', 'q_from_mass1_mass2', 'invq_from_mass1_mass2', 'eta_from_mass1_mass2', 'mchirp_from_mass1_mass2', 'mass1_from_mtotal_q', 'mass2_from_mtotal_q', 'mass1_from_mtotal_eta', 'mass2_from_mtotal_eta', 'mtotal_from_mchirp_eta', 'mass1_from_mchirp_eta', 'mass2_from_mchirp_eta', 'mass2_from_mchirp_mass1', 'mass_from_knownmass_eta', 'mass2_from_mass1_eta', 'mass1_from_mass2_eta', 'eta_from_q', 'mass1_from_mchirp_q', 'mass2_from_mchirp_q', 'tau0_from_mtotal_eta', 'tau3_from_mtotal_eta', 'tau0_from_mass1_mass2', 'tau0_from_mchirp', 'mchirp_from_tau0', 'tau3_from_mass1_mass2', 'mtotal_from_tau0_tau3', 'eta_from_tau0_tau3', 'mass1_from_tau0_tau3', 'mass2_from_tau0_tau3', 'primary_spin', 'secondary_spin', 'chi_eff', 'chi_a', 'chi_p', 'phi_a', 'phi_s', 'primary_xi', 'secondary_xi', 'xi1_from_spin1x_spin1y', 'xi2_from_mass1_mass2_spin2x_spin2y', 'chi_perp_from_spinx_spiny', 'chi_perp_from_mass1_mass2_xi2', 'chi_p_from_xi1_xi2', 'phi_from_spinx_spiny', 'phi1_from_phi_a_phi_s', 'phi2_from_phi_a_phi_s', 'spin1z_from_mass1_mass2_chi_eff_chi_a', 'spin2z_from_mass1_mass2_chi_eff_chi_a', 'spin1x_from_xi1_phi_a_phi_s', 'spin1y_from_xi1_phi_a_phi_s', 'spin2x_from_mass1_mass2_xi2_phi_a_phi_s', 'spin2y_from_mass1_mass2_xi2_phi_a_phi_s', 'chirp_distance', 'det_tc', 'snr_from_loglr', 'freq_from_final_mass_spin', 'tau_from_final_mass_spin', 'final_spin_from_f0_tau', 'final_mass_from_f0_tau', 'final_mass_from_initial', 'final_spin_from_initial', 'optimal_dec_from_detector', 'optimal_ra_from_detector', 'chi_eff_from_spherical', 'chi_p_from_spherical', 'nltides_gw_phase_diff_isco', 'spin_from_pulsar_freq', 'freqlmn_from_other_lmn', 'taulmn_from_other_lmn', 'remnant_mass_from_mass1_mass2_spherical_spin_eos', 'remnant_mass_from_mass1_mass2_cartesian_spin_eos', 'lambda1_from_delta_lambda_tilde_lambda_tilde', 'lambda2_from_delta_lambda_tilde_lambda_tilde', 'delta_lambda_tilde' ]