From b4049d49dbbb2be02828f4e393f3807d9ced1a73 Mon Sep 17 00:00:00 2001 From: Connor Weaving <73482302+ConWea@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:34:38 +0100 Subject: [PATCH] Lisa dev merge (#4128) * fix waveform_transforms not defined issue * add static_args back * fix cc issues * adopt Ian's changes for LISA * update for LISA bank * fix self.ta[ifo] issue * fix * fix ta issue * remove all changes * Plugin testing script * remove all hard-coded "BBHx" * Rel bug fix * Begin no atenna response wf interface * remove old bbhx plugin * add general interface for BBHx * is_lisa issue not fix yet * Adding test scripts * Plot code for inference * Approximant name change * Changed is_lisa flag to checking approximant name against type of waveform being used. * Modified cython code with own version of likelihood_parts function for waveforms whcih compute own det response * Error in changing to dictionary output from plugin * Fixes to PR request. Mainly reconfiguring due to convention choice of returning data as dict with their data and channel name * Code rename conventions * Changes from codeclimate * More codeclimate changes * More codeclimate changes Co-authored-by: WuShichao --- bin/bank/pycbc_brute_bank | 4 +- pycbc/inference/models/relbin.py | 137 +++++++++++++++++--------- pycbc/inference/models/relbin_cpu.pyx | 31 ++++++ pycbc/psd/__init__.py | 7 +- pycbc/strain/strain.py | 2 +- pycbc/waveform/parameters.py | 15 ++- pycbc/waveform/plugin.py | 26 +++-- pycbc/waveform/waveform.py | 43 +++++++- 8 files changed, 202 insertions(+), 63 deletions(-) diff --git a/bin/bank/pycbc_brute_bank b/bin/bank/pycbc_brute_bank index 29274acc082..3b3ef417674 100644 --- a/bin/bank/pycbc_brute_bank +++ b/bin/bank/pycbc_brute_bank @@ -272,8 +272,10 @@ class GenUniformWaveform(object): def generate(self, **kwds): kwds.update(fdict) if kwds['approximant'] in pycbc.waveform.fd_approximants(): - hp, hc = pycbc.waveform.get_fd_waveform(delta_f=self.delta_f, + ws = pycbc.waveform.get_fd_waveform(delta_f=self.delta_f, f_lower=self.f_lower, **kwds) + hp = ws[0] + hc = ws[1] if 'fratio' in kwds: hp = hc * kwds['fratio'] + hp * (1 - kwds['fratio']) else: diff --git a/pycbc/inference/models/relbin.py b/pycbc/inference/models/relbin.py index bb897cd8bf2..4fe6dd86aa9 100644 --- a/pycbc/inference/models/relbin.py +++ b/pycbc/inference/models/relbin.py @@ -31,13 +31,15 @@ import itertools from scipy.interpolate import interp1d -from pycbc.waveform import get_fd_waveform_sequence +from pycbc.waveform import (get_fd_waveform_sequence, + get_fd_det_waveform_sequence, fd_det_sequence) from pycbc.detector import Detector from pycbc.types import Array from .gaussian_noise import BaseGaussianNoise from .relbin_cpu import (likelihood_parts, likelihood_parts_v, - likelihood_parts_multi, likelihood_parts_multi_v) + likelihood_parts_multi, likelihood_parts_multi_v, + likelihood_parts_det) from .tools import DistMarg @@ -169,10 +171,17 @@ def __init__( variable_params, data, low_frequency_cutoff, **kwargs ) + # If the waveform handles the detector response internally, set + # self.det_response = True + self.no_det_response = False + if self.static_params['approximant'] in fd_det_sequence: + self.no_det_response = True + # reference waveform and bin edges self.f, self.df, self.end_time, self.det = {}, {}, {}, {} self.h00, self.h00_sparse = {}, {} self.fedges, self.edges = {}, {} + self.ta = {} self.antenna_time = {} # filtered summary data for linear approximation @@ -181,6 +190,7 @@ def __init__( # store fiducial waveform params self.fid_params = self.static_params.copy() self.fid_params.update(fiducial_params) + for k in self.static_params: if self.fid_params[k] == 'REPLACE': self.fid_params.pop(k) @@ -191,7 +201,7 @@ def __init__( self.f[ifo] = numpy.array(d0.sample_frequencies) self.df[ifo] = d0.delta_f self.end_time[ifo] = float(d0.end_time) - self.det[ifo] = Detector(ifo) + # self.det[ifo] = Detector(ifo) # generate fiducial waveform f_lo = self.kmin[ifo] * self.df[ifo] @@ -204,12 +214,20 @@ def __init__( # prune low frequency samples to avoid waveform errors fpoints = Array(self.f[ifo].astype(numpy.float64)) fpoints = fpoints[self.kmin[ifo]:self.kmax[ifo]+1] - fid_hp, fid_hc = get_fd_waveform_sequence(sample_points=fpoints, - **self.fid_params) + + if self.no_det_response: + wave = get_fd_det_waveform_sequence(ifos=ifo, + sample_points=fpoints, + **self.fid_params) + curr_wav = wave[ifo] + else: + fid_hp, fid_hc = get_fd_waveform_sequence(sample_points=fpoints, + **self.fid_params) + curr_wav = fid_hp # check for zeros at high frequencies # make sure only nonzero samples are included in bins - numzeros = list(fid_hp[::-1] != 0j).index(True) + numzeros = list(curr_wav[::-1] != 0j).index(True) if numzeros > 0: new_kmax = self.kmax[ifo] - numzeros f_hi = new_kmax * self.df[ifo] @@ -219,27 +237,36 @@ def __init__( "will be %s Hz", f_hi) # make copy of fiducial wfs, adding back in low frequencies - fid_hp.resize(len(self.f[ifo])) - fid_hc.resize(len(self.f[ifo])) - hp0 = numpy.roll(fid_hp, self.kmin[ifo]) - hc0 = numpy.roll(fid_hc, self.kmin[ifo]) - - # get detector-specific arrival times relative to end of data - dt = self.det[ifo].time_delay_from_earth_center( - self.fid_params["ra"], - self.fid_params["dec"], - self.fid_params["tc"], - ) + if self.no_det_response: + curr_wav.resize(len(self.f[ifo])) + curr_wav = numpy.roll(curr_wav, self.kmin[ifo]) + # get detector-specific arrival times relative to end of data + self.ta[ifo] = -self.end_time[ifo] + tshift = numpy.exp(-2.0j * numpy.pi * self.f[ifo] * self.ta[ifo]) + h00 = numpy.array(curr_wav) * tshift + self.h00[ifo] = h00 + else: + fid_hp.resize(len(self.f[ifo])) + fid_hc.resize(len(self.f[ifo])) + hp0 = numpy.roll(fid_hp, self.kmin[ifo]) + hc0 = numpy.roll(fid_hc, self.kmin[ifo]) - ta = self.fid_params["tc"] + dt - self.end_time[ifo] - tshift = numpy.exp(-2.0j * numpy.pi * self.f[ifo] * ta) + self.det[ifo] = Detector(ifo) + dt = self.det[ifo].time_delay_from_earth_center( + self.fid_params["ra"], + self.fid_params["dec"], + self.fid_params["tc"], + ) + self.ta = self.fid_params["tc"] + dt - self.end_time[ifo] - fp, fc = self.det[ifo].antenna_pattern( - self.fid_params["ra"], self.fid_params["dec"], - self.fid_params["polarization"], self.fid_params["tc"]) + fp, fc = self.det[ifo].antenna_pattern( + self.fid_params["ra"], self.fid_params["dec"], + self.fid_params["polarization"], self.fid_params["tc"]) - h00 = (hp0 * fp + hc0 * fc) * tshift - self.h00[ifo] = h00 + tshift = numpy.exp(-2.0j * numpy.pi * self.f[ifo] * self.ta) + + h00 = (hp0 * fp + hc0 * fc) * tshift + self.h00[ifo] = h00 # compute frequency bins logging.info("Computing frequency bins") @@ -313,7 +340,10 @@ def setup_antenna(self, earth_rotation, fedges): self.mlik = likelihood_parts_multi_v else: atimes = self.fid_params["tc"] - self.lik = likelihood_parts + if self.no_det_response: + self.lik = likelihood_parts_det + else: + self.lik = likelihood_parts self.mlik = likelihood_parts_multi return atimes @@ -340,13 +370,22 @@ def summary_product(self, h1, h2, bins, ifo): def get_waveforms(self, params): """ Get the waveform polarizations for each ifo """ + if self.no_det_response: + wfs = {} + for ifo in self.data: + wfs = wfs | get_fd_det_waveform_sequence(ifos=ifo, + sample_points=self.fedges[ifo], + **params) + return wfs + wfs = [] for edge in self.edge_unique: hp, hc = get_fd_waveform_sequence(sample_points=edge, **params) hp = hp.numpy() hc = hc.numpy() wfs.append((hp, hc)) - return {ifo: wfs[self.ifo_map[ifo]] for ifo in self.data} + wf_ret = {ifo: wfs[self.ifo_map[ifo]] for ifo in self.data} + return wf_ret @property def multi_signal_support(self): @@ -435,34 +474,42 @@ def _loglr(self): p.update(self.static_params) wfs = self.get_waveforms(p) - hh = 0.0 - hd = 0j + norm = 0.0 + filt = 0j self._current_wf_parts = {} for ifo in self.data: - det = self.det[ifo] freqs = self.fedges[ifo] sdat = self.sdat[ifo] h00 = self.h00_sparse[ifo] end_time = self.end_time[ifo] times = self.antenna_time[ifo] - # project waveform to detector frame - hp, hc = wfs[ifo] - fp, fc = det.antenna_pattern(p["ra"], p["dec"], - p["polarization"], times) - dt = det.time_delay_from_earth_center(p["ra"], p["dec"], times) - dtc = p["tc"] + dt - end_time - - hdp, hhp = self.lik(freqs, fp, fc, dtc, - hp, hc, h00, - sdat['a0'], sdat['a1'], - sdat['b0'], sdat['b1']) - - self._current_wf_parts[ifo] = (fp, fc, dtc, hp, hc, h00) - hd += hdp - hh += hhp - return self.marginalize_loglr(hd, hh) + # project waveform to detector frame if waveform does not deal + # with detector response. Otherwise, skip detector response. + + if self.no_det_response: + dtc = -end_time + channel = wfs[ifo].numpy() + filter_i, norm_i = self.lik(freqs, dtc, channel, h00, + sdat['a0'], sdat['a1'], + sdat['b0'], sdat['b1']) + else: + hp, hc = wfs[ifo] + det = self.det[ifo] + fp, fc = det.antenna_pattern(p["ra"], p["dec"], + p["polarization"], times) + dt = det.time_delay_from_earth_center(p["ra"], p["dec"], times) + dtc = p["tc"] + dt - end_time + + filter_i, norm_i = self.lik(freqs, fp, fc, dtc, + hp, hc, h00, + sdat['a0'], sdat['a1'], + sdat['b0'], sdat['b1']) + self._current_wf_parts[ifo] = (fp, fc, dtc, hp, hc, h00) + filt += filter_i + norm += norm_i + return self.marginalize_loglr(filt, norm) def write_metadata(self, fp, group=None): """Adds writing the fiducial parameters and epsilon to file's attrs. diff --git a/pycbc/inference/models/relbin_cpu.pyx b/pycbc/inference/models/relbin_cpu.pyx index 88f11f49e11..91541707933 100644 --- a/pycbc/inference/models/relbin_cpu.pyx +++ b/pycbc/inference/models/relbin_cpu.pyx @@ -107,6 +107,37 @@ cpdef likelihood_parts(double [::1] freqs, x0 = x0n return hd, hh +cpdef likelihood_parts_det(double [::1] freqs, + double dtc, + double complex[::1] hp, + double complex[::1] h00, + double complex[::1] a0, + double complex[::1] a1, + double [::1] b0, + double [::1] b1, + ) : + cdef size_t i + cdef double complex hd=0, r0, r0n, r1, x0, x1, x0n; + cdef double hh=0 + cdef int N + + N = freqs.shape[0] + for i in range(N): + r0n = (exp(-2.0j * 3.141592653 * dtc * freqs[i]) + * (hp[i])) / h00[i] + r1 = r0n - r0 + + x0n = norm(r0n) + x1 = x0n - x0 + + if i > 0: + hd += a0[i-1] * r0 + a1[i-1] * r1 + hh += real(b0[i-1] * x0 + b1[i-1] * x1) + + r0 = r0n + x0 = x0n + return hd, hh + cpdef likelihood_parts_v(double [::1] freqs, double[::1] fp, double[::1] fc, diff --git a/pycbc/psd/__init__.py b/pycbc/psd/__init__.py index 756e8650be5..4738d26e9dc 100644 --- a/pycbc/psd/__init__.py +++ b/pycbc/psd/__init__.py @@ -65,7 +65,7 @@ def from_cli(opt, length, delta_f, low_frequency_cutoff, The frequency series containing the PSD. """ f_low = low_frequency_cutoff - sample_rate = int((length -1) * 2 * delta_f) + sample_rate = (length -1) * 2 * delta_f try: psd_estimation = opt.psd_estimation is not None @@ -109,13 +109,14 @@ def from_cli(opt, length, delta_f, low_frequency_cutoff, elif psd_estimation: # estimate PSD from data psd = welch(strain, avg_method=opt.psd_estimation, - seg_len=int(opt.psd_segment_length * sample_rate), - seg_stride=int(opt.psd_segment_stride * sample_rate), + seg_len=int(opt.psd_segment_length * sample_rate + 0.5), + seg_stride=int(opt.psd_segment_stride * sample_rate + 0.5), num_segments=opt.psd_num_segments, require_exact_data_fit=False) if delta_f != psd.delta_f: psd = interpolate(psd, delta_f) + else: # Shouldn't be possible to get here raise ValueError("Shouldn't be possible to raise this!") diff --git a/pycbc/strain/strain.py b/pycbc/strain/strain.py index 3b204c802a3..ed152eb8625 100644 --- a/pycbc/strain/strain.py +++ b/pycbc/strain/strain.py @@ -844,7 +844,7 @@ def insert_strain_option_group_multi_ifo(parser, gps_times=True): '--hdf-store']) required_opts_list = ['--gps-start-time', '--gps-end-time', - '--strain-high-pass', '--pad-data', '--sample-rate', + '--pad-data', '--sample-rate', '--channel-name'] diff --git a/pycbc/waveform/parameters.py b/pycbc/waveform/parameters.py index b0c2ec51093..925fff39a93 100644 --- a/pycbc/waveform/parameters.py +++ b/pycbc/waveform/parameters.py @@ -418,13 +418,13 @@ def docstr(self, prefix='', include_label=True): label=r"$\Delta t_c~(\rm{s})$", description="Coalesence time offset.") ra = Parameter("ra", - dtype=float, default=None, label=r"$\alpha$", + dtype=float, default=0., label=r"$\alpha$", description="Right ascension (rad).") dec = Parameter("dec", - dtype=float, default=None, label=r"$\delta$", + dtype=float, default=0., label=r"$\delta$", description="Declination (rad).") polarization = Parameter("polarization", - dtype=float, default=None, label=r"$\psi$", + dtype=float, default=0., label=r"$\psi$", description="Polarization (rad).") redshift = Parameter("redshift", dtype=float, default=None, label=r"$z$", @@ -432,6 +432,12 @@ def docstr(self, prefix='', include_label=True): comoving_volume = Parameter("comoving_volume", dtype=float, label=r"$V_C~(\rm{Mpc}^3)$", description="Comoving volume (in cubic Mpc).") +eclipticlatitude = Parameter("eclipticlatitude", + dtype=float, default=0., label=r"$\beta$", + description="eclipticlatitude wrt SSB coords.") +eclipticlongitude = Parameter("eclipticlongitude", + dtype=float, default=0., label=r"$\lambda$", + description="eclipticlongitude wrt SSB coords.") # # Calibration parameters @@ -547,7 +553,8 @@ def docstr(self, prefix='', include_label=True): # passed to the waveform generators in lalsimulation, but are instead applied # after a waveform is generated. Distance, however, is a parameter used by # the waveform generators. -location_params = ParameterList([tc, ra, dec, polarization]) +location_params = ParameterList([tc, ra, dec, polarization, + eclipticlatitude, eclipticlongitude]) # parameters describing the orientation of a binary w.r.t. the radiation # frame. Note: we include distance here, as it is typically used for generating diff --git a/pycbc/waveform/plugin.py b/pycbc/waveform/plugin.py index f47ae9f989b..3f1ccd8901e 100644 --- a/pycbc/waveform/plugin.py +++ b/pycbc/waveform/plugin.py @@ -3,7 +3,8 @@ def add_custom_waveform(approximant, function, domain, - sequence=False, force=False): + sequence=False, has_det_response=False, + force=False,): """ Make custom waveform available to pycbc Parameters @@ -17,8 +18,11 @@ def add_custom_waveform(approximant, function, domain, sequence : bool, False Function evaluates waveform at only chosen points (instead of a equal-spaced grid). + has_det_response : bool, False + Check if waveform generator has built-in detector response. """ - from pycbc.waveform.waveform import cpu_fd, cpu_td, fd_sequence + from pycbc.waveform.waveform import (cpu_fd, cpu_td, fd_sequence, + fd_det_sequence) used = RuntimeError("Can't load plugin waveform {}, the name is" " already in use.".format(approximant)) @@ -29,9 +33,14 @@ def add_custom_waveform(approximant, function, domain, cpu_td[approximant] = function elif domain == 'frequency': if sequence: - if not force and (approximant in fd_sequence): - raise used - fd_sequence[approximant] = function + if not has_det_response: + if not force and (approximant in fd_sequence): + raise used + fd_sequence[approximant] = function + else: + if not force and (approximant in fd_det_sequence): + raise used + fd_det_sequence[approximant] = function else: if not force and (approximant in cpu_fd): raise used @@ -67,11 +76,16 @@ def retrieve_waveform_plugins(): for plugin in pkg_resources.iter_entry_points('pycbc.waveform.fd'): add_custom_waveform(plugin.name, plugin.resolve(), 'frequency') - # Check for fd sequence waveforms + # Check for fd sequence waveforms (no detector response) for plugin in pkg_resources.iter_entry_points('pycbc.waveform.fd_sequence'): add_custom_waveform(plugin.name, plugin.resolve(), 'frequency', sequence=True) + # Check for fd sequence waveforms (has detector response) + for plugin in pkg_resources.iter_entry_points('pycbc.waveform.fd_det_sequence'): + add_custom_waveform(plugin.name, plugin.resolve(), 'frequency', + sequence=True, has_det_response=True) + # Check for td waveforms for plugin in pkg_resources.iter_entry_points('pycbc.waveform.td'): add_custom_waveform(plugin.name, plugin.resolve(), 'time') diff --git a/pycbc/waveform/waveform.py b/pycbc/waveform/waveform.py index d4d5887e9d3..7ad0059fe7c 100644 --- a/pycbc/waveform/waveform.py +++ b/pycbc/waveform/waveform.py @@ -467,6 +467,7 @@ def props_sgburst(obj, **kwargs): # Waveform generation ######################################################## fd_sequence = {} +fd_det_sequence = {} def _lalsim_fd_sequence(**p): """ Shim to interface to lalsimulation SimInspiralChooseFDWaveformSequence @@ -490,9 +491,10 @@ def _lalsim_fd_sequence(**p): for apx in _lalsim_enum: fd_sequence[apx] = _lalsim_fd_sequence + def get_fd_waveform_sequence(template=None, **kwds): """Return values of the waveform evaluated at the sequence of frequency - points. + points. The waveform generator doesn't include detector response. Parameters ---------- @@ -524,10 +526,44 @@ def get_fd_waveform_sequence(template=None, **kwds): check_args(input_params, required) return wav_gen(**input_params) +def get_fd_det_waveform_sequence(template=None, **kwds): + """Return values of the waveform evaluated at the sequence of frequency + points. The waveform generator includes detector response. + + Parameters + ---------- + template: object + An object that has attached properties. This can be used to substitute + for keyword arguments. A common example would be a row in an xml table. + {params} + + Returns + ------- + dict + The detector-frame waveform (with detector response) in frequency + domain evaluated at the frequency points. Keys are requested data + channels. + """ + input_params = props(template, **kwds) + input_params['delta_f'] = -1 + input_params['f_lower'] = -1 + if input_params['approximant'] not in fd_det_sequence: + raise ValueError("Approximant %s not available" % + (input_params['approximant'])) + wav_gen = fd_det_sequence[input_params['approximant']] + if hasattr(wav_gen, 'required'): + required = wav_gen.required + else: + required = parameters.fd_required + check_args(input_params, required) + return wav_gen(**input_params) get_fd_waveform_sequence.__doc__ = get_fd_waveform_sequence.__doc__.format( params=parameters.fd_waveform_sequence_params.docstr(prefix=" ", include_label=False).lstrip(' ')) +get_fd_det_waveform_sequence.__doc__ = get_fd_det_waveform_sequence.__doc__.format( + params=parameters.fd_waveform_sequence_params.docstr(prefix=" ", + include_label=False).lstrip(' ')) def get_td_waveform(template=None, **kwargs): """Return the plus and cross polarizations of a time domain waveform. @@ -1231,7 +1267,7 @@ def get_waveform_filter_length_in_time(approximant, template=None, **kwargs): return None __all__ = ["get_td_waveform", "get_fd_waveform", "get_fd_waveform_sequence", - "get_fd_waveform_from_td", + "get_fd_det_waveform_sequence", "get_fd_waveform_from_td", "print_td_approximants", "print_fd_approximants", "td_approximants", "fd_approximants", "get_waveform_filter", "filter_approximants", @@ -1241,4 +1277,5 @@ def get_waveform_filter_length_in_time(approximant, template=None, **kwargs): "print_sgburst_approximants", "sgburst_approximants", "td_waveform_to_fd_waveform", "get_two_pol_waveform_filter", "NoWaveformError", "FailedWaveformError", "get_td_waveform_from_fd", - 'cpu_fd', 'cpu_td', 'fd_sequence', '_filter_time_lengths'] + 'cpu_fd', 'cpu_td', 'fd_sequence', 'fd_det_sequence', + '_filter_time_lengths']