Skip to content
Draft
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ classifiers = [
requires-python = ">=3.11, <3.13"
dependencies = [
"numpy==1.26.4",
"lalsuite==7.25.1",
"astropy==7.0",
"h5py~=3.11",
"arviz~=0.19",
"pandas~=2.2",
Expand All @@ -39,6 +39,12 @@ ringdown_scan = "ringdown.cli.ringdown_scan:main"
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[project.optional-dependencies]
extra = [
"lalsuite==7.25.1",
"gwpy==3.0.10",
]

[tool.uv]
dev-dependencies = [
"ringdown",
Expand Down
19 changes: 9 additions & 10 deletions ringdown/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import numpy as np
import scipy.signal as sig
import lal
import scipy.linalg as sl
from scipy.interpolate import interp1d
import scipy.signal as ss
Expand Down Expand Up @@ -510,15 +509,15 @@ def __init__(self, *args, ifo=None, attrs=None, **kwargs):
def _constructor(self):
return Data

@property
def detector(self) -> lal.Detector:
""":mod:`lal` object containing detector information.
"""
if self.ifo:
d = lal.cached_detector_by_prefix[self.ifo]
else:
d = None
return d
# @property
# def detector(self) -> lal.Detector:
# """:mod:`lal` object containing detector information.
# """
# if self.ifo:
# d = lal.cached_detector_by_prefix[self.ifo]
# else:
# d = None
# return d

def condition(self, t0: float | None = None,
ds: int | None = None,
Expand Down
897 changes: 897 additions & 0 deletions ringdown/detector.py

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions ringdown/fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import numpyro
import jaxlib.xla_extension
import xarray as xr
import lal
import logging
from .data import Data, AutoCovariance, PowerSpectrum
from . import utils
Expand Down Expand Up @@ -1666,7 +1665,7 @@ def set_target(
"""
# turn float into LIGOTimeGPS object to ensure we get the right
# number of digits when converting to string
t0 = lal.LIGOTimeGPS(t0) if isinstance(t0, float) else t0
# t0 = lal.LIGOTimeGPS(t0) if isinstance(t0, float) else t0

# record all arguments for provenance
settings = {k: v for k, v in locals().items() if k != "self"}
Expand Down
16 changes: 9 additions & 7 deletions ringdown/imr.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
get_dict_from_pattern,
)
from .config import IMR_CONFIG_SECTION, WHITENED_LOGLIKE_KEY
import lal
import multiprocessing as mp
from lalsimulation import nrfits
import logging
import inspect

logger = logging.getLogger(__name__)

MSUN_SI = 1.988409870698050677689968230400e30

MASS_ALIASES = [
"final_mass",
"mf",
Expand Down Expand Up @@ -59,6 +59,8 @@ def get_remnant(
f_ref,
model,
):
# lazy import LAL (optional dependency)
from lalsimulation import nrfits
r = nrfits.eval_nrfit(
mass_1,
mass_2,
Expand Down Expand Up @@ -391,8 +393,8 @@ def get_remnant_parameters(

if nproc is None:
r = np.vectorize(get_remnant)(
self["mass_1"] * lal.MSUN_SI,
self["mass_2"] * lal.MSUN_SI,
self["mass_1"] * MSUN_SI,
self["mass_2"] * MSUN_SI,
self["spin_1x"],
self["spin_1y"],
self["spin_1z"],
Expand All @@ -407,8 +409,8 @@ def get_remnant_parameters(
r = p.starmap(
get_remnant,
zip(
self["mass_1"] * lal.MSUN_SI,
self["mass_2"] * lal.MSUN_SI,
self["mass_1"] * MSUN_SI,
self["mass_2"] * MSUN_SI,
self["spin_1x"],
self["spin_1y"],
self["spin_1z"],
Expand All @@ -421,7 +423,7 @@ def get_remnant_parameters(
)

r = np.array(r).reshape(len(self), 2)
self["final_mass"] = r[:, 0] / lal.MSUN_SI
self["final_mass"] = r[:, 0] / MSUN_SI
self["final_spin"] = r[:, 1]
return self[keys]

Expand Down
41 changes: 20 additions & 21 deletions ringdown/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
__all__ = ["Target", "SkyTarget", "DetectorTarget", "TargetCollection"]

import numpy as np
import lal
from astropy.time import Time
import logging
from dataclasses import dataclass, asdict
from abc import ABC, abstractmethod
from .utils import utils
from .utils.utils import try_parse
from .config import T_MSUN, IMR_CONFIG_SECTION, PIPE_SECTION
from . import detector

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -134,7 +135,7 @@ def construct(
class SkyTarget(Target):
"""Sky location target for a ringdown analysis."""

geocenter_time: float | lal.LIGOTimeGPS | None = None
geocenter_time: float | Time | None = None
ra: float | None = None
dec: float | None = None
psi: float | None = None
Expand All @@ -143,7 +144,7 @@ class SkyTarget(Target):
def __post_init__(self):
# validate input: floats or None
for k, v in self.as_dict().items():
if v is not None and not isinstance(v, lal.LIGOTimeGPS):
if v is not None and not isinstance(v, Time):
setattr(self, k, float(v))
# make sure options are not contradictory
if self.is_set:
Expand Down Expand Up @@ -177,12 +178,11 @@ def get_detector_time(self, ifo) -> float:
times : float
time for specified detector.
"""
if ifo in lal.cached_detector_by_prefix:
det = lal.cached_detector_by_prefix[ifo]
else:
if ifo not in detector.KNOWN_IFOS:
raise ValueError(f"unrecognized detector {ifo}")
tgps = lal.LIGOTimeGPS(self.geocenter_time)
dt = lal.TimeDelayFromEarthCenter(det.location, self.ra, self.dec, tgps)
dt_func = detector.get_geocenter_delay_function(ifo)
gmst = detector.gmst_from_gps(self.geocenter_time)
dt = dt_func(self.ra, self.dec, gmst)
t0 = self.geocenter_time + dt
return float(t0)

Expand All @@ -199,16 +199,16 @@ def get_antenna_patterns(self, ifo) -> tuple[float, float]:
antenna_patterns : dict
dictionary of antenna patterns.
"""
if ifo in lal.cached_detector_by_prefix:
det = lal.cached_detector_by_prefix[ifo]
else:
if ifo not in detector.KNOWN_IFOS:
raise ValueError(f"unrecognized detector {ifo}")
tgps = lal.LIGOTimeGPS(self.geocenter_time)
gmst = lal.GreenwichMeanSiderealTime(tgps)
fpfc = lal.ComputeDetAMResponse(
det.response, self.ra, self.dec, self.psi, gmst
)
return tuple(fpfc)
# get plus and cross functions
fp_func = detector.get_antenna_pattern_function(ifo, 'p')
fc_func = detector.get_antenna_pattern_function(ifo, 'c')
# evaluate at requested parameters
gmst = detector.gmst_from_gps(self.geocenter_time)
fp = float(fp_func(self.ra, self.dec, self.psi, gmst))
fc = float(fc_func(self.ra, self.dec, self.psi, gmst))
return fp, fc

@classmethod
def construct(
Expand Down Expand Up @@ -247,13 +247,12 @@ def construct(
if reference_ifo is None:
tgeo = t0
else:
det = lal.cached_detector_by_prefix[reference_ifo]
tgps = lal.LIGOTimeGPS(t0)
dt = lal.TimeDelayFromEarthCenter(det.location, ra, dec, tgps)
dt_func = detector.get_geocenter_delay_function(reference_ifo)
dt = float(dt_func(ra, dec, detector.gmst_from_gps(t0)))
tgeo = t0 - dt
if kws:
logger.info(f"unused keyword arguments: {kws}")
return cls(lal.LIGOTimeGPS(tgeo), ra, dec, psi, duration)
return cls(np.float64(tgeo), ra, dec, psi, duration)

@property
def settings(self) -> dict:
Expand Down
60 changes: 41 additions & 19 deletions ringdown/waveforms/coalescence.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
__all__ = ["Coalescence", "Parameters"]

import numpy as np
import lal

from .core import Signal, _ishift
from ..utils import docstring_parameter

try:
from scipy.signal.windows import tukey
except ImportError:
from scipy.signal import tukey
import lalsimulation as ls
from dataclasses import dataclass, asdict, fields
import inspect
import h5py
import logging

logger = logging.getLogger(__name__)

MSUN_SI = 1.988409870698050677689968230400e30
PC_SI = 3.08567758149136720000e16
GMSUN_SI = 1.32712440000000000000e20
C_SI = 2.99792458000000000000e08

try:
import lalsimulation as ls
MODELS = [
ls.GetStringFromApproximant(a)
for a in range(ls.NumApproximants)
if ls.SimInspiralImplementedFDApproximants(a)
or ls.SimInspiralImplementedTDApproximants(a)
]
except ImportError:
MODELS = []


def m1m2_from_mtotq(mtot, q):
m1 = mtot / (1 + q)
Expand Down Expand Up @@ -215,6 +230,9 @@ def construct(cls, **kws):
pars : Parameters
coalescence parameters container object
""".format(cls._ALIASES_STR)
# lazy import of LAL (optional dependency)
import lalsimulation as ls

kws["f_ref"] = kws.get("f_ref", kws.get("f_low"))
for par, aliases in cls._ALIASES.items():
for k in aliases:
Expand All @@ -239,8 +257,8 @@ def construct(cls, **kws):
if not all(lsim_given) and any(linf_given):
try:
a = [kws[k] for k in cls._SPIN_KEYS_LALINF] + [
kws["mass_1"] * lal.MSUN_SI,
kws["mass_2"] * lal.MSUN_SI,
kws["mass_1"] * MSUN_SI,
kws["mass_2"] * MSUN_SI,
kws["f_ref"],
kws["phase"],
]
Expand Down Expand Up @@ -304,17 +322,17 @@ def cos_iota(self):
@property
def luminosity_distance_si(self):
"""Luminosity distance in meters."""
return self.luminosity_distance * 1e6 * lal.PC_SI
return self.luminosity_distance * 1e6 * PC_SI

@property
def mass_1_si(self):
"""First component mass in kg."""
return self.mass_1 * lal.MSUN_SI
return self.mass_1 * MSUN_SI

@property
def mass_2_si(self):
"""Second component mass in kg."""
return self.mass_2 * lal.MSUN_SI
return self.mass_2 * MSUN_SI

def compute_remnant_mchi(
self, model: str = "NRSur7dq4Remnant", solar_masses=True
Expand All @@ -333,13 +351,16 @@ def compute_remnant_mchi(
chif : float
remnant dimensionless spin magnitude.
"""
# lazy import LAL (optional dependency)
from lalsimulation import nrfits

if solar_masses:
m1 = self["mass_1"] * lal.MSUN_SI
m2 = self["mass_2"] * lal.MSUN_SI
m1 = self["mass_1"] * MSUN_SI
m2 = self["mass_2"] * MSUN_SI
else:
m1 = self["mass_1"]
m2 = self["mass_2"]
remnant = ls.nrfits.eval_nrfit(
remnant = nrfits.eval_nrfit(
m1,
m2,
[self["spin_1x"], self["spin_1y"], self["spin_1z"]],
Expand All @@ -350,7 +371,7 @@ def compute_remnant_mchi(
)
mf = remnant["FinalMass"]
if solar_masses:
mf /= lal.MSUN_SI
mf /= MSUN_SI
chif = np.linalg.norm(remnant["FinalSpin"])
return mf, chif

Expand All @@ -368,11 +389,11 @@ def final_spin(self):

@property
def final_mass_seconds(self):
return self.final_mass * lal.GMSUN_SI / lal.C_SI**3
return self.final_mass * GMSUN_SI / C_SI**3

@property
def final_mass_si(self):
return self.final_mass * lal.MSUN_SI
return self.final_mass * MSUN_SI

def get_choosetdwaveform_args(self, delta_t):
"""Construct input for :func:`ls.SimInspiralChooseTDWaveform`.
Expand Down Expand Up @@ -506,14 +527,9 @@ class Coalescence(Signal):
"""An inspiral-merger-ringdown signal from a compact binary coalescence."""

_DEF_TUKEY_ALPHA = 0.125
_MODELS = MODELS

# register names of all available LALSimulation approximants
_MODELS = [
ls.GetStringFromApproximant(a)
for a in range(ls.NumApproximants)
if ls.SimInspiralImplementedFDApproximants(a)
or ls.SimInspiralImplementedTDApproximants(a)
]

def __init__(self, *args, modes=None, **kwargs):
super(Coalescence, self).__init__(*args, **kwargs)
Expand Down Expand Up @@ -602,6 +618,10 @@ def from_parameters(
h : Coalescence
compact binary coalescence signal.
"""
# lazy import of LAL (optional dependency)
import lal
import lalsimulation as ls

approximant = model or approximant

if approximant is None:
Expand Down Expand Up @@ -779,6 +799,8 @@ def get_invariant_peak_time(self, ell_max=None, force=False):
t_peak : float
peak time of the invariant strain
"""
# lazy import LAL (optional dependency)
import lal

# LALDict *LALpars,
# /**< LAL dictionary containing accessory parameters */
Expand Down
Loading