diff --git a/bilby/gw/__init__.py b/bilby/gw/__init__.py index 3273cbd3e8358d2d91a3a44249c6dac3d00db287..d7ded8db4c4741c6c491e88a1c722447d3e2ba49 100644 --- a/bilby/gw/__init__.py +++ b/bilby/gw/__init__.py @@ -1,4 +1,4 @@ -from . import (calibration, conversion, detector, likelihood, prior, source, +from . import (calibration, conversion, detector, likelihood, prior, series, source, utils, waveform_generator) from .waveform_generator import WaveformGenerator diff --git a/bilby/gw/detector.py b/bilby/gw/detector.py index d87ce2aaf8e9d6256424271de6ea82b8b4857d7b..03fb5b2ebef2ca780c8d5fcc23455e78d6b87e3a 100644 --- a/bilby/gw/detector.py +++ b/bilby/gw/detector.py @@ -11,6 +11,7 @@ from . import utils as gwutils from ..core import utils from ..core.utils import logger from .calibration import Recalibrate +from .series import CoupledTimeAndFrequencySeries try: import gwpy @@ -233,57 +234,14 @@ class InterferometerStrainData(object): self.roll_off = roll_off self.window_factor = 1 - self._set_time_and_frequency_array_parameters(None, None, None) + self._times_and_frequencies = CoupledTimeAndFrequencySeries() + # self._set_time_and_frequency_array_parameters(None, None, None) self._frequency_domain_strain = None self._frequency_array = None self._time_domain_strain = None self._time_array = None - @property - def frequency_array(self): - """ Frequencies of the data in Hz """ - if self._frequency_array is not None: - return self._frequency_array - else: - self._calculate_frequency_array() - return self._frequency_array - - @frequency_array.setter - def frequency_array(self, frequency_array): - self._frequency_array = frequency_array - - @property - def time_array(self): - """ Time of the data in seconds """ - if self._time_array is not None: - return self._time_array - else: - self._calculate_time_array() - return self._time_array - - @time_array.setter - def time_array(self, time_array): - self._time_array = time_array - - def _calculate_time_array(self): - """ Calculate the time array """ - if (self.sampling_frequency is None) or (self.duration is None): - raise ValueError( - "You have not specified the sampling_frequency and duration") - - self.time_array = utils.create_time_series( - sampling_frequency=self.sampling_frequency, duration=self.duration, - starting_time=self.start_time) - - def _calculate_frequency_array(self): - """ Calculate the frequency array """ - if (self.sampling_frequency is None) or (self.duration is None): - raise ValueError( - "You have not specified the sampling_frequency and duration") - self.frequency_array = utils.create_frequency_series( - sampling_frequency=self.sampling_frequency, duration=self.duration) - def time_within_data(self, time): """ Check if time is within the data span @@ -503,10 +461,9 @@ class InterferometerStrainData(object): elif array is not None: if domain == 'time': self.time_array = array - sampling_frequency, duration = utils.get_sampling_frequency_and_duration_from_time_array(array) elif domain == 'frequency': self.frequency_array = array - sampling_frequency, duration = utils.get_sampling_frequency_and_duration_from_frequency_array(array) + return elif sampling_frequency is None or duration is None: raise ValueError( "You must provide both sampling_frequency and duration") @@ -744,9 +701,51 @@ class InterferometerStrainData(object): self.set_from_gwpy_timeseries(strain) def _set_time_and_frequency_array_parameters(self, duration, sampling_frequency, start_time): - self.sampling_frequency = sampling_frequency - self.duration = duration - self.start_time = start_time + self._times_and_frequencies = CoupledTimeAndFrequencySeries(duration=duration, + sampling_frequency=sampling_frequency, + start_time=start_time) + + @property + def sampling_frequency(self): + return self._times_and_frequencies.sampling_frequency + + @sampling_frequency.setter + def sampling_frequency(self, sampling_frequency): + self._times_and_frequencies.sampling_frequency = sampling_frequency + + @property + def duration(self): + return self._times_and_frequencies.duration + + @duration.setter + def duration(self, duration): + self._times_and_frequencies.duration = duration + + @property + def start_time(self): + return self._times_and_frequencies.start_time + + @start_time.setter + def start_time(self, start_time): + self._times_and_frequencies.start_time = start_time + + @property + def frequency_array(self): + """ Frequencies of the data in Hz """ + return self._times_and_frequencies.frequency_array + + @frequency_array.setter + def frequency_array(self, frequency_array): + self._times_and_frequencies.frequency_array = frequency_array + + @property + def time_array(self): + """ Time of the data in seconds """ + return self._times_and_frequencies.time_array + + @time_array.setter + def time_array(self, time_array): + self._times_and_frequencies.time_array = time_array class Interferometer(object): diff --git a/bilby/gw/series.py b/bilby/gw/series.py new file mode 100644 index 0000000000000000000000000000000000000000..aab94dee94d96db575af6ae6ee42c7b719250f7b --- /dev/null +++ b/bilby/gw/series.py @@ -0,0 +1,123 @@ +from ..core import utils + + +class CoupledTimeAndFrequencySeries(object): + + def __init__(self, duration=None, sampling_frequency=None, start_time=0): + """ A waveform generator + + Parameters + ---------- + sampling_frequency: float, optional + The sampling frequency + duration: float, optional + Time duration of data + start_time: float, optional + Starting time of the time array + """ + self.duration = duration + self.sampling_frequency = sampling_frequency + self.start_time = start_time + self._frequency_array_updated = False + self._time_array_updated = False + + @property + def frequency_array(self): + """ Frequency array for the waveforms. Automatically updates if sampling_frequency or duration are updated. + + Returns + ------- + array_like: The frequency array + """ + if self._frequency_array_updated is False: + if self.sampling_frequency and self.duration: + self.frequency_array = utils.create_frequency_series( + sampling_frequency=self.sampling_frequency, + duration=self.duration) + else: + raise ValueError('Can not calculate a frequency series without a ' + 'legitimate sampling_frequency ({}) or duration ({})' + .format(self.sampling_frequency, self.duration)) + + return self._frequency_array + + @frequency_array.setter + def frequency_array(self, frequency_array): + self._frequency_array = frequency_array + self._sampling_frequency, self._duration = \ + utils.get_sampling_frequency_and_duration_from_frequency_array(frequency_array) + self._frequency_array_updated = True + + @property + def time_array(self): + """ Time array for the waveforms. Automatically updates if sampling_frequency or duration are updated. + + Returns + ------- + array_like: The time array + """ + + if self._time_array_updated is False: + if self.sampling_frequency and self.duration: + self._time_array = utils.create_time_series( + sampling_frequency=self.sampling_frequency, + duration=self.duration, + starting_time=self.start_time) + else: + raise ValueError('Can not calculate a time series without a ' + 'legitimate sampling_frequency ({}) or duration ({})' + .format(self.sampling_frequency, self.duration)) + + self._time_array_updated = True + return self._time_array + + @time_array.setter + def time_array(self, time_array): + self._time_array = time_array + self._sampling_frequency, self._duration = \ + utils.get_sampling_frequency_and_duration_from_time_array(time_array) + self._start_time = time_array[0] + self._time_array_updated = True + + @property + def duration(self): + """ Allows one to set the time duration and automatically updates the frequency and time array. + + Returns + ------- + float: The time duration. + + """ + return self._duration + + @duration.setter + def duration(self, duration): + self._duration = duration + self._frequency_array_updated = False + self._time_array_updated = False + + @property + def sampling_frequency(self): + """ Allows one to set the sampling frequency and automatically updates the frequency and time array. + + Returns + ------- + float: The sampling frequency. + + """ + return self._sampling_frequency + + @sampling_frequency.setter + def sampling_frequency(self, sampling_frequency): + self._sampling_frequency = sampling_frequency + self._frequency_array_updated = False + self._time_array_updated = False + + @property + def start_time(self): + return self._start_time + + @start_time.setter + def start_time(self, start_time): + self._start_time = start_time + self._time_array_updated = False diff --git a/bilby/gw/waveform_generator.py b/bilby/gw/waveform_generator.py index 2166326170930c8caa7ee10241490895b48e01d1..6e9135731291f0f80172fea753583d5d5d22c2f0 100644 --- a/bilby/gw/waveform_generator.py +++ b/bilby/gw/waveform_generator.py @@ -1,5 +1,6 @@ import numpy as np from ..core import utils +from ..gw.series import CoupledTimeAndFrequencySeries class WaveformGenerator(object): @@ -41,6 +42,9 @@ class WaveformGenerator(object): the WaveformGenerator object and initialised to `None`. """ + self._times_and_frequencies = CoupledTimeAndFrequencySeries(duration=duration, + sampling_frequency=sampling_frequency, + start_time=start_time) self.duration = duration self.sampling_frequency = sampling_frequency self.start_time = start_time @@ -57,8 +61,6 @@ class WaveformGenerator(object): self.waveform_arguments = dict() if isinstance(parameters, dict): self.parameters = parameters - self.__frequency_array_updated = False - self.__time_array_updated = False def __repr__(self): if self.frequency_domain_source_model is not None: @@ -171,48 +173,6 @@ class WaveformGenerator(object): model_strain[key] = transformation_function(transformed_model_strain[key], self.sampling_frequency) return model_strain - @property - def frequency_array(self): - """ Frequency array for the waveforms. Automatically updates if sampling_frequency or duration are updated. - - Returns - ------- - array_like: The frequency array - """ - if self.__frequency_array_updated is False: - self.frequency_array = utils.create_frequency_series( - self.sampling_frequency, - self.duration) - return self.__frequency_array - - @frequency_array.setter - def frequency_array(self, frequency_array): - self.__frequency_array = frequency_array - self.__frequency_array_updated = True - - @property - def time_array(self): - """ Time array for the waveforms. Automatically updates if sampling_frequency or duration are updated. - - Returns - ------- - array_like: The time array - """ - - if self.__time_array_updated is False: - self.__time_array = utils.create_time_series( - self.sampling_frequency, - self.duration, - self.start_time) - - self.__time_array_updated = True - return self.__time_array - - @time_array.setter - def time_array(self, time_array): - self.__time_array = time_array - self.__time_array_updated = True - @property def parameters(self): """ The dictionary of parameters for source model. @@ -265,6 +225,34 @@ class WaveformGenerator(object): 'model must be provided.') return set(utils.infer_parameters_from_function(model)) + @property + def frequency_array(self): + """ Frequency array for the waveforms. Automatically updates if sampling_frequency or duration are updated. + + Returns + ------- + array_like: The frequency array + """ + return self._times_and_frequencies.frequency_array + + @frequency_array.setter + def frequency_array(self, frequency_array): + self._times_and_frequencies.frequency_array = frequency_array + + @property + def time_array(self): + """ Time array for the waveforms. Automatically updates if sampling_frequency or duration are updated. + + Returns + ------- + array_like: The time array + """ + return self._times_and_frequencies.time_array + + @time_array.setter + def time_array(self, time_array): + self._times_and_frequencies.time_array = time_array + @property def duration(self): """ Allows one to set the time duration and automatically updates the frequency and time array. @@ -274,13 +262,11 @@ class WaveformGenerator(object): float: The time duration. """ - return self.__duration + return self._times_and_frequencies.duration @duration.setter def duration(self, duration): - self.__duration = duration - self.__frequency_array_updated = False - self.__time_array_updated = False + self._times_and_frequencies.duration = duration @property def sampling_frequency(self): @@ -291,19 +277,16 @@ class WaveformGenerator(object): float: The sampling frequency. """ - return self.__sampling_frequency + return self._times_and_frequencies.sampling_frequency @sampling_frequency.setter def sampling_frequency(self, sampling_frequency): - self.__sampling_frequency = sampling_frequency - self.__frequency_array_updated = False - self.__time_array_updated = False + self._times_and_frequencies.sampling_frequency = sampling_frequency @property def start_time(self): - return self.__start_time + return self._times_and_frequencies.start_time @start_time.setter - def start_time(self, starting_time): - self.__start_time = starting_time - self.__time_array_updated = False + def start_time(self, start_time): + self._times_and_frequencies.start_time = start_time diff --git a/test/detector_test.py b/test/detector_test.py index 59efc53c8cb1e0986868e3c2a9a1263895882699..7ca202e6de1305c3e54e0194343cf06e5472cdd5 100644 --- a/test/detector_test.py +++ b/test/detector_test.py @@ -459,20 +459,20 @@ class TestInterferometerStrainData(unittest.TestCase): self.ifosd.frequency_domain_strain == frequency_domain_strain * self.ifosd.frequency_mask)) def test_time_array_when_set(self): - test_array = np.array([1]) + test_array = np.array([1, 2, 3]) self.ifosd.time_array = test_array - self.assertTrue(test_array, self.ifosd.time_array) - - @patch.object(bilby.core.utils, 'create_time_series') - def test_time_array_when_not_set(self, m): - self.ifosd.start_time = 3 - self.ifosd.sampling_frequency = 1000 - self.ifosd.duration = 5 - m.return_value = 4 - self.assertEqual(m.return_value, self.ifosd.time_array) - m.assert_called_with(sampling_frequency=self.ifosd.sampling_frequency, - duration=self.ifosd.duration, - starting_time=self.ifosd.start_time) + self.assertTrue(np.array_equal(test_array, self.ifosd.time_array)) + + def test_time_array_when_not_set(self): + with mock.patch('bilby.core.utils.create_time_series') as m: + self.ifosd.start_time = 3 + self.ifosd.sampling_frequency = 1000 + self.ifosd.duration = 5 + m.return_value = 4 + self.assertEqual(m.return_value, self.ifosd.time_array) + m.assert_called_with(sampling_frequency=1000, + duration=5, + starting_time=3) def test_time_array_without_sampling_frequency(self): self.ifosd.sampling_frequency = None @@ -487,18 +487,18 @@ class TestInterferometerStrainData(unittest.TestCase): test = self.ifosd.time_array def test_frequency_array_when_set(self): - test_array = np.array([1]) + test_array = np.array([1, 2, 3]) self.ifosd.frequency_array = test_array - self.assertTrue(test_array, self.ifosd.frequency_array) + self.assertTrue(np.array_equal(test_array, self.ifosd.frequency_array)) - @patch.object(bilby.core.utils, 'create_frequency_series') - def test_time_array_when_not_set(self, m): - self.ifosd.sampling_frequency = 1000 - self.ifosd.duration = 5 - m.return_value = 4 - self.assertEqual(m.return_value, self.ifosd.frequency_array) - m.assert_called_with(sampling_frequency=self.ifosd.sampling_frequency, - duration=self.ifosd.duration) + def test_frequency_array_when_not_set(self): + with mock.patch('bilby.core.utils.create_frequency_series') as m: + m.return_value = [1, 2, 3] + self.ifosd.sampling_frequency = 1000 + self.ifosd.duration = 5 + self.assertListEqual(m.return_value, self.ifosd.frequency_array) + m.assert_called_with(sampling_frequency=1000, + duration=5) def test_frequency_array_without_sampling_frequency(self): self.ifosd.sampling_frequency = None @@ -565,8 +565,8 @@ class TestInterferometerStrainData(unittest.TestCase): m.return_value = 5 self.ifosd.sampling_frequency = 200 self.ifosd.duration = 4 - self.ifosd._frequency_domain_strain = self.ifosd.frequency_array self.ifosd.sampling_frequency = 123 + self.ifosd.frequency_domain_strain = self.ifosd.frequency_array self.assertEqual(m.return_value, self.ifosd.time_domain_strain) def test_time_domain_strain_not_set(self): @@ -719,14 +719,32 @@ class TestInterferometerList(unittest.TestCase): self.assertEqual(self.ifo2, ifo_list[1]) def test_init_inconsistent_duration(self): + self.frequency_arrays = np.linspace(0, 2048, 2049) + self.ifo2 = bilby.gw.detector.Interferometer(name=self.name2, + power_spectral_density=self.power_spectral_density2, + minimum_frequency=self.minimum_frequency2, + maximum_frequency=self.maximum_frequency2, length=self.length2, + latitude=self.latitude2, longitude=self.longitude2, + elevation=self.elevation2, + xarm_azimuth=self.xarm_azimuth2, yarm_azimuth=self.yarm_azimuth2, + xarm_tilt=self.xarm_tilt2, yarm_tilt=self.yarm_tilt2) self.ifo2.strain_data.set_from_frequency_domain_strain( - np.linspace(0, 4096, 4097), sampling_frequency=4096, duration=3) + self.frequency_arrays, sampling_frequency=4096, duration=1) with self.assertRaises(ValueError): bilby.gw.detector.InterferometerList([self.ifo1, self.ifo2]) def test_init_inconsistent_sampling_frequency(self): + self.frequency_arrays = np.linspace(0, 2048, 2049) + self.ifo2 = bilby.gw.detector.Interferometer(name=self.name2, + power_spectral_density=self.power_spectral_density2, + minimum_frequency=self.minimum_frequency2, + maximum_frequency=self.maximum_frequency2, length=self.length2, + latitude=self.latitude2, longitude=self.longitude2, + elevation=self.elevation2, + xarm_azimuth=self.xarm_azimuth2, yarm_azimuth=self.yarm_azimuth2, + xarm_tilt=self.xarm_tilt2, yarm_tilt=self.yarm_tilt2) self.ifo2.strain_data.set_from_frequency_domain_strain( - np.linspace(0, 4096, 4097), sampling_frequency=234, duration=2) + self.frequency_arrays, sampling_frequency=2048, duration=2) with self.assertRaises(ValueError): bilby.gw.detector.InterferometerList([self.ifo1, self.ifo2])