# Copyright (C) 2023, 2024, 2025 Cecilio García Quirós
"""
Define generators to interface with gwsignal.
"""
try:
from lalsimulation.gwsignal.core.waveform import CompactBinaryCoalescenceGenerator
import astropy.units as u
from gwpy.timeseries import TimeSeries
from gwpy.frequencyseries import FrequencySeries
except:
raise ImportWarning("Cannot use gwsignal interface for phenomxpy.")
import phenomxpy
from phenomxpy.utils import parse_options_from_approximant_name, convert_params
try:
from lalsimulation.gwsignal.core.parameter_conventions import default_dict
except ImportError:
import astropy.units as u
default_dict = {
"mass1": 1.0 * u.solMass,
"mass2": 1.0 * u.solMass,
"spin1x": 0.0 * u.dimensionless_unscaled,
"spin1y": 0.0 * u.dimensionless_unscaled,
"spin1z": 0.0 * u.dimensionless_unscaled,
"spin2x": 0.0 * u.dimensionless_unscaled,
"spin2y": 0.0 * u.dimensionless_unscaled,
"spin2z": 0.0 * u.dimensionless_unscaled,
"deltaT": 1.0 / 64.0 * u.s,
"f22_start": 20.0 * u.Hz,
"f22_ref": 20.0 * u.Hz,
"phi_ref": 0.0 * u.rad,
"distance": 1.0 * u.Mpc,
"inclination": 0.0 * u.rad,
"eccentricity": 0.0 * u.dimensionless_unscaled,
"longAscNodes": 0.0 * u.rad,
"meanPerAno": 0.0 * u.rad,
}
[docs]
def strip_units(waveform_dict):
"""
Remove units from astropy dictionary.
"""
new_dc = {}
for key in waveform_dict.keys():
new_dc[key] = waveform_dict[key].value if isinstance(waveform_dict[key], u.Quantity) else waveform_dict[key]
return new_dc
[docs]
def convert_params_from_gwsignal(input_params):
"""
Convert gwsignal (astropy dictionary) to phenomxpy.
Parameters are transformed to the units in the default_dict of gwsignal.
Units are removed.
Parameters
----------
input_params: gwsiganl dictionary
parameters with astropy units.
Returns
-------
dict
Parameters in phenomxpy format without units.
"""
# Deep copy to not modify the original dictionary
params = input_params.copy()
# Conversion to units used in phenomxpy. It uses the same units as defined in gwsignal.core.parameter_conventions.default_dict
for key in params:
if key in default_dict and hasattr(params[key], "unit") and params[key].unit is not u.dimensionless_unscaled:
params[key] = params[key].to(default_dict[key].unit)
# Renaming some keys
for old_key, new_key in {
"f22_start": "f_min",
"f22_ref": "f_ref",
"deltaT": "delta_t",
"deltaF": "delta_f",
"ModeArray": "mode_array",
}.items():
if old_key in params:
params[new_key] = params.pop(old_key)
return convert_params(strip_units(params))
[docs]
class PyIMRPhenomT(CompactBinaryCoalescenceGenerator):
"""
gwsignal generator wrapper for the IMRPhenomT model.
This is the parent class from which all the other IMRPhenomT* models subclass.
Example usage:
.. code-block:: python
from lalsimulation import gwsignal as gws
gen = PyIMRPhenomT()
hp, hc = gws.core.waveform.GenerateTDWaveform(params, gen)
"""
def __init__(self, **kwargs):
self._domain = "time"
self._implemented_domain = "time"
self._generation_domain = None
self._approximant = "IMRPhenomT"
# This imports the corresponding Phenom class but doesn't create the instance.
# Instances are created inside the methods `generate_td_waveform`, etc.
self._class = getattr(phenomxpy, self._approximant)
@property
def metadata(self):
return self._class.metadata()
def _evaluate_class(self, **parameters):
"""
Create instance of the IMRPhenom class (initialization of coefficients).
"""
# Convert from gwsiganl dictionary to phenomxpy dictionary
self.waveform_dict = convert_params_from_gwsignal(parameters)
return self._class(**self.waveform_dict)
[docs]
class PyIMRPhenomTHM(PyIMRPhenomT):
"""
gwsignal generator wrapper for the IMRPhenomTHM model.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._approximant = "IMRPhenomTHM"
self._class = getattr(phenomxpy, self._approximant)
[docs]
class PyIMRPhenomTP(PyIMRPhenomT):
"""
gwsignal generator wrapper for the IMRPhenomTP model.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._approximant = "IMRPhenomTP"
self._class = getattr(phenomxpy, self._approximant)
[docs]
class PyIMRPhenomTPHM(PyIMRPhenomT):
"""
gwsignal generator wrapper for the IMRPhenomTPHM model.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._approximant = "IMRPhenomTPHM"
self._class = getattr(phenomxpy, self._approximant)
[docs]
class PyIMRPhenomTFamily(PyIMRPhenomT):
"""
gwsignal generator wrapper for the IMRPhenomTFamily.
This class permits to use all the models in the IMRPhenomT family with the same generator.
One needs to specify the `approximant` name in the input parameters.
Example usage:
.. code-block:: python
gen = PyIMRPhenomTFamily()
params["approximant"] = "IMRPhenomTHM"
hp, hc = gws.core.waveform.GenerateTDWaveform(params, gen)
params["approximant"] = "IMRPhenomTPHM"
hp, hc = gws.core.waveform.GenerateTDWaveform(params, gen)
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._approximant = "IMRPhenomTFamily"
@property
def metadata(self):
return {
"type": "aligned_spin, precessing",
"f_ref_spin": True,
"modes": True,
"polarizations": True,
"implemented_domain": "time",
"approximant": "IMRPhenomTFamily",
"implementation": "",
"conditioning_routines": "",
}
def _evaluate_class(self, **parameters):
# Update parameters with options read from approximant name
# Returns approximant name without options, i.e. IMRPhenomT, IMRPhenomTHM, ...
approx_wo_options = parse_options_from_approximant_name(parameters)
# Choose appropiate phenomxpy class from approximant name
try:
self._class = getattr(phenomxpy, approx_wo_options)
except KeyError as e:
raise KeyError(f"Using PyIMRPhenomTFamily needs to provide `approximant` key in `parameters`.") from e
self.waveform_dict = convert_params_from_gwsignal(parameters)
return self._class(**self.waveform_dict)