Skip to content
Snippets Groups Projects
Commit 379a6ed6 authored by Rhiannon Udall's avatar Rhiannon Udall Committed by Colm Talbot
Browse files

ENH: Add whitened TD strain as a property of the Interferometer

parent d4c02aa0
No related branches found
No related tags found
1 merge request!1367ENH: Add whitened TD strain as a property of the Interferometer
Pipeline #643669 failed
...@@ -616,19 +616,95 @@ class Interferometer(object): ...@@ -616,19 +616,95 @@ class Interferometer(object):
power_spectral_density=self.power_spectral_density_array[self.strain_data.frequency_mask], power_spectral_density=self.power_spectral_density_array[self.strain_data.frequency_mask],
duration=self.strain_data.duration) duration=self.strain_data.duration)
def whiten_frequency_series(self, frequency_series : np.array) -> np.array:
"""Whitens a frequency series with the noise properties of the detector
.. math::
\\tilde{a}_w(f) = \\tilde{a}(f) \\sqrt{\\frac{4}{T S_n(f)}}
Such that
.. math::
Var(n) = \\frac{1}{N} \\sum_k=0^N n_W(f_k)n_W^*(f_k) = 2
Where the factor of two is due to the independent real and imaginary
components.
Parameters
==========
frequency_series : np.array
The frequency series, whitened by the ASD
"""
return frequency_series / (self.amplitude_spectral_density_array * np.sqrt(self.duration / 4))
def get_whitened_time_series_from_whitened_frequency_series(
self,
whitened_frequency_series : np.array
) -> np.array:
"""Gets the whitened time series from a whitened frequency series.
This ifft's and also applies a windowing factor,
since when f_min and f_max are set bilby applies a mask to the series.
Per 6.2a-b in https://arxiv.org/pdf/gr-qc/0509116 since our window
is just a band pass,
this coefficient is :math:`w/W` where
.. math::
W = \\frac{1}{N} \\sum_{k=0}^N w^2[j]
Since our window :math:`w` is simply 1 or 0, depending on the mask, we get
.. math::
W = \\frac{1}{N} \\sum_{k=0}^N \\Theta(f_{max} - f_k)\\Theta(f_k - f_{min})}
and accordingly the termwise window factor is
.. math::
w = \\sqrt{N W} = \\sqrt{\\sum_{k=0}^N \\Theta(f_{max} - f_k)\\Theta(f_k - f_{min})}}
"""
frequency_window_factor = (
np.sum(self.frequency_mask)
/ len(self.frequency_mask)
)
whitened_time_series = (
np.fft.irfft(whitened_frequency_series)
* np.sqrt(np.sum(self.frequency_mask)) / frequency_window_factor
)
return whitened_time_series
@property @property
def whitened_frequency_domain_strain(self): def whitened_frequency_domain_strain(self):
""" Calculates the whitened data by dividing the frequency domain data by r"""Whitens the frequency domain data by dividing through by ASD,
((amplitude spectral density) * (duration / 4) ** 0.5). The resulting with appropriate normalization.
data will have unit variance.
See `whiten_frequency_series()` for details.
Returns Returns
======= =======
array_like: The whitened data array_like: The whitened data
""" """
return self.strain_data.frequency_domain_strain / ( return self.whiten_frequency_series(self.strain_data.frequency_domain_strain)
self.amplitude_spectral_density_array * np.sqrt(self.duration / 4)
) @property
def whitened_time_domain_strain(self) -> np.array:
"""Calculates the whitened time domain strain
by iffting the whitened frequency domain strain,
with the appropriate normalization.
See `get_whitened_time_series_from_whitened_frequency_series()` for details
Returns
=======
array_like
The whitened data in the time domain
"""
return self.get_whitened_time_series_from_whitened_frequency_series(self.whitened_frequency_domain_strain)
def save_data(self, outdir, label=None): def save_data(self, outdir, label=None):
""" Creates save files for interferometer data in plain text format. """ Creates save files for interferometer data in plain text format.
......
...@@ -377,10 +377,6 @@ class CompactBinaryCoalescenceResult(CoreResult): ...@@ -377,10 +377,6 @@ class CompactBinaryCoalescenceResult(CoreResult):
logger.debug("Downsampling frequency mask to {} values".format( logger.debug("Downsampling frequency mask to {} values".format(
len(frequency_idxs)) len(frequency_idxs))
) )
frequency_window_factor = (
np.sum(interferometer.frequency_mask)
/ len(interferometer.frequency_mask)
)
plot_times = interferometer.time_array[time_idxs] plot_times = interferometer.time_array[time_idxs]
plot_times -= interferometer.strain_data.start_time plot_times -= interferometer.strain_data.start_time
start_time -= interferometer.strain_data.start_time start_time -= interferometer.strain_data.start_time
...@@ -451,11 +447,7 @@ class CompactBinaryCoalescenceResult(CoreResult): ...@@ -451,11 +447,7 @@ class CompactBinaryCoalescenceResult(CoreResult):
fig.add_trace( fig.add_trace(
go.Scatter( go.Scatter(
x=plot_times, x=plot_times,
y=np.fft.irfft( y=interferometer.whitened_time_domain_strain[time_idxs],
interferometer.whitened_frequency_domain_strain
* np.sqrt(np.sum(interferometer.frequency_mask))
/ frequency_window_factor
)[time_idxs],
fill=None, fill=None,
mode='lines', line_color=DATA_COLOR, mode='lines', line_color=DATA_COLOR,
opacity=0.5, opacity=0.5,
...@@ -478,11 +470,7 @@ class CompactBinaryCoalescenceResult(CoreResult): ...@@ -478,11 +470,7 @@ class CompactBinaryCoalescenceResult(CoreResult):
interferometer.amplitude_spectral_density_array[frequency_idxs], interferometer.amplitude_spectral_density_array[frequency_idxs],
color=DATA_COLOR, label='ASD') color=DATA_COLOR, label='ASD')
axs[1].plot( axs[1].plot(
plot_times, np.fft.irfft( plot_times, interferometer.whitened_time_domain_strain[time_idxs],
interferometer.whitened_frequency_domain_strain
* np.sqrt(np.sum(interferometer.frequency_mask))
/ frequency_window_factor
)[time_idxs],
color=DATA_COLOR, alpha=0.3) color=DATA_COLOR, alpha=0.3)
logger.debug('Plotted interferometer data.') logger.debug('Plotted interferometer data.')
...@@ -493,10 +481,10 @@ class CompactBinaryCoalescenceResult(CoreResult): ...@@ -493,10 +481,10 @@ class CompactBinaryCoalescenceResult(CoreResult):
wf_pols = waveform_generator.frequency_domain_strain(params) wf_pols = waveform_generator.frequency_domain_strain(params)
fd_waveform = interferometer.get_detector_response(wf_pols, params) fd_waveform = interferometer.get_detector_response(wf_pols, params)
fd_waveforms.append(fd_waveform[frequency_idxs]) fd_waveforms.append(fd_waveform[frequency_idxs])
td_waveform = infft( whitened_fd_waveform = interferometer.whiten_frequency_series(fd_waveform)
fd_waveform * np.sqrt(2. / interferometer.sampling_frequency) / td_waveform = interferometer.get_whitened_time_series_from_whitened_frequency_series(
interferometer.amplitude_spectral_density_array, whitened_fd_waveform
self.sampling_frequency)[time_idxs] )[time_idxs]
td_waveforms.append(td_waveform) td_waveforms.append(td_waveform)
fd_waveforms = asd_from_freq_series( fd_waveforms = asd_from_freq_series(
fd_waveforms, fd_waveforms,
......
...@@ -560,21 +560,92 @@ class TestInterferometerAntennaPatternAgainstLAL(unittest.TestCase): ...@@ -560,21 +560,92 @@ class TestInterferometerAntennaPatternAgainstLAL(unittest.TestCase):
class TestInterferometerWhitenedStrain(unittest.TestCase): class TestInterferometerWhitenedStrain(unittest.TestCase):
def setUp(self): def setUp(self):
self.duration = 64
self.sampling_frequency = 4096
self.ifo = bilby.gw.detector.get_empty_interferometer('H1') self.ifo = bilby.gw.detector.get_empty_interferometer('H1')
self.ifo.set_strain_data_from_power_spectral_density( self.ifo.set_strain_data_from_power_spectral_density(
sampling_frequency=4096, duration=64) sampling_frequency=self.sampling_frequency, duration=self.duration)
self.waveform_generator = bilby.gw.waveform_generator.WaveformGenerator(
duration=self.duration,
sampling_frequency=self.sampling_frequency,
frequency_domain_source_model=bilby.gw.source.lal_binary_black_hole,
waveform_arguments={
"waveform_approximant": "IMRPhenomXP"
})
self.parameters = {
'mass_1': 10,
'mass_2': 10,
'a_1': 0,
'a_2': 0,
'tilt_1': 0,
'tilt_2': 0,
'phi_12': 0,
'phi_jl': 0,
'theta_jn': 0,
'luminosity_distance': 40,
'phase': 0,
'ra': 0,
'dec': 0,
'geocent_time': 62,
'psi': 0
}
def tearDown(self): def tearDown(self):
del self.ifo del self.ifo
del self.waveform_generator
del self.parameters
del self.duration
del self.sampling_frequency
def test_whitened_strain(self): def _check_frequency_series_whiteness(self, frequency_series):
mask = self.ifo.frequency_mask std_real = np.std(frequency_series.real)
white = self.ifo.whitened_frequency_domain_strain[mask] std_imag = np.std(frequency_series.imag)
std_real = np.std(white.real)
std_imag = np.std(white.imag)
self.assertAlmostEqual(std_real, 1, places=2) self.assertAlmostEqual(std_real, 1, places=2)
self.assertAlmostEqual(std_imag, 1, places=2) self.assertAlmostEqual(std_imag, 1, places=2)
def _check_time_series_whiteness(self, time_series):
std = np.std(time_series)
self.assertAlmostEqual(std, 1, places=2)
if __name__ == "__main__": def test_frequency_domain_whitened_strain(self):
unittest.main() mask = self.ifo.frequency_mask
white = self.ifo.whitened_frequency_domain_strain[mask]
self._check_frequency_series_whiteness(white)
def test_time_domain_whitened_strain(self):
whitened_td = self.ifo.whitened_time_domain_strain
self._check_time_series_whiteness(whitened_td)
def test_frequency_domain_noise_and_signal_whitening(self):
# Inject some (loud) signal
self.ifo.inject_signal(waveform_generator=self.waveform_generator, parameters=self.parameters)
# Make the template separately
waveform_polarizations = self.waveform_generator.frequency_domain_strain(parameters=self.parameters)
signal_ifo = self.ifo.get_detector_response(
waveform_polarizations=waveform_polarizations,
parameters=self.parameters
)
# Whiten the template
whitened_signal_ifo = self.ifo.whiten_frequency_series(signal_ifo)
mask = self.ifo.frequency_mask
white = self.ifo.whitened_frequency_domain_strain[mask] - whitened_signal_ifo[mask]
self._check_frequency_series_whiteness(white)
def test_time_domain_noise_and_signal_whitening(self):
# Inject some (loud) signal
self.ifo.inject_signal(waveform_generator=self.waveform_generator, parameters=self.parameters)
# Make the template separately
waveform_polarizations = self.waveform_generator.frequency_domain_strain(parameters=self.parameters)
signal_ifo = self.ifo.get_detector_response(
waveform_polarizations=waveform_polarizations,
parameters=self.parameters
)
# Whiten the template in FD
whitened_signal_ifo_fd = self.ifo.whiten_frequency_series(signal_ifo)
# Get whitened template in TD
whitened_signal_ifo_td = self.ifo.get_whitened_time_series_from_whitened_frequency_series(
whitened_signal_ifo_fd
)
whitened_td = self.ifo.whitened_time_domain_strain - whitened_signal_ifo_td
self._check_time_series_whiteness(whitened_td)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment