diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 57f2ae08e01439daa7592866666a07962542e741..f36f51088dadcbf390f50c6c6ee03e23b3d19b5f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -31,6 +31,7 @@ exitcode-jessie:
     - coverage erase
     - coverage run --source /usr/local/lib/python2.7/dist-packages/tupak/ -a test/conversion_tests.py
     - coverage run --source /usr/local/lib/python2.7/dist-packages/tupak/ -a test/detector_tests.py
+    - coverage run --source /usr/local/lib/python2.7/dist-packages/tupak/ -a test/utils_tests.py
     - coverage run --source /usr/local/lib/python2.7/dist-packages/tupak/ -a test/prior_tests.py
     - coverage run --source /usr/local/lib/python2.7/dist-packages/tupak/ -a test/sampler_tests.py
     - coverage run --source /usr/local/lib/python2.7/dist-packages/tupak/ -a test/waveform_generator_tests.py
diff --git a/requirements.txt b/requirements.txt
index 356634c1c863b2c80a7d6f84efbf66d16a6056c7..2466c12067f514d4effcfe7c2945c32829453348 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@ dynesty
 corner
 numpy>=1.9
 matplotlib>=2.0
-scipy
+scipy>=0.16
 gwpy
 pandas
 astropy
diff --git a/test/detector_tests.py b/test/detector_tests.py
index da3ecfa14e0e9f782477c5d8d8f5c88522f8f6a9..4b6abf9666b2e77c813df2f3b322a6b9ffc10dda 100644
--- a/test/detector_tests.py
+++ b/test/detector_tests.py
@@ -358,6 +358,23 @@ class TestInterferometerStrainData(unittest.TestCase):
             sampling_frequency=sampling_frequency, start_time=10)
         self.assertTrue(self.ifosd.start_time == 10)
 
+    def test_time_array_frequency_array_consistency(self):
+        duration = 1
+        sampling_frequency = 10
+        time_array = tupak.core.utils.create_time_series(
+            sampling_frequency=sampling_frequency, duration=duration)
+        time_domain_strain = np.random.normal(0, 1, len(time_array))
+        self.ifosd.roll_off = 0
+        self.ifosd.set_from_time_domain_strain(
+            time_domain_strain=time_domain_strain, duration=duration,
+            sampling_frequency=sampling_frequency)
+
+        frequency_domain_strain, freqs = tupak.core.utils.nfft(
+            time_domain_strain, sampling_frequency)
+
+        self.assertTrue(np.all(
+            frequency_domain_strain == self.ifosd.frequency_domain_strain))
+
     #def test_sampling_duration_init(self):
     #    self.assertIsNone(self.ifo.duration)
 
diff --git a/test/utils_tests.py b/test/utils_tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..9c4285e283d1be2c996dd17ead14505c8e2733b3
--- /dev/null
+++ b/test/utils_tests.py
@@ -0,0 +1,34 @@
+from __future__ import absolute_import, division
+
+import tupak
+import unittest
+import numpy as np
+import matplotlib.pyplot as plt
+
+
+class TestFFT(unittest.TestCase):
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_nfft_frequencies(self):
+        f = 2.1
+        sampling_frequency = 10
+        times = np.arange(0, 100, 1/sampling_frequency)
+        tds = np.sin(2*np.pi*times * f + 0.4)
+        fds, freqs = tupak.core.utils.nfft(tds, sampling_frequency)
+        self.assertTrue(np.abs((f-freqs[np.argmax(np.abs(fds))])/f < 1e-15))
+
+    def test_nfft_infft(self):
+        sampling_frequency = 10
+        tds = np.random.normal(0, 1, 10)
+        fds, _ = tupak.core.utils.nfft(tds, sampling_frequency)
+        tds2 = tupak.core.utils.infft(fds, sampling_frequency)
+        self.assertTrue(np.all(np.abs((tds - tds2) / tds) < 1e-12))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tupak/core/utils.py b/tupak/core/utils.py
index 86be692f942e5a7ee0178b068cf0fd7c3f33ffae..74704d931327e7fc5a95b14cd20e5d8376d46168 100644
--- a/tupak/core/utils.py
+++ b/tupak/core/utils.py
@@ -33,6 +33,25 @@ def get_sampling_frequency(time_series):
         return 1. / (time_series[1] - time_series[0])
 
 
+def get_sampling_frequency_and_duration_from_time_array(time_array):
+    """
+    Calculate sampling frequency and duration from a time array
+
+    Returns
+    -------
+    sampling_frequency, duration:
+
+    Raises
+    -------
+    ValueError: If the time_array is not evenly sampled.
+
+    """
+
+    sampling_frequency = get_sampling_frequency(time_array)
+    duration = time_array[-1] - time_array[0]
+    return sampling_frequency, duration
+
+
 def get_sampling_frequency_and_duration_from_frequency_array(frequency_array):
     """
     Calculate sampling frequency and duration from a frequency array
@@ -201,59 +220,68 @@ def create_white_noise(sampling_frequency, duration):
     return white_noise, frequencies
 
 
-def nfft(ht, Fs):
-    """ Performs an FFT while keeping track of the frequency bins assumes input time series is real
-        (positive frequencies only)
+def nfft(time_domain_strain, sampling_frequency):
+    """ Perform an FFT while keeping track of the frequency bins. Assumes input
+        time series is real (positive frequencies only).
 
     Parameters
-    -------
-    ht: array_like
-        Time series
-    Fs: float
-        Sampling frequency
+    ----------
+    time_domain_strain: array_like
+        Time series of strain data.
+    sampling_frequency: float
+        Sampling frequency of the data.
 
     Returns
     -------
-    array_like: Single-sided FFT of ft normalised to units of strain / sqrt(Hz) (hf)
-    array_like: Frequencies associated with hf
+    frequency_domain_strain, frequency_array: (array, array)
+        Single-sided FFT of time domain strain normalised to units of
+        strain / Hz, and the associated frequency_array.
+
     """
-    # add one zero padding if time series does not have even number of sampling times
-    if np.mod(len(ht), 2) == 1:
-        ht = np.append(ht, 0)
-    LL = len(ht)
+
+    if np.ndim(sampling_frequency) != 0:
+        raise ValueError("Sampling frequency must be interger or float")
+
+    # add one zero padding if time series doesn't have even number of samples
+    if np.mod(len(time_domain_strain), 2) == 1:
+        time_domain_strain = np.append(time_domain_strain, 0)
+    LL = len(time_domain_strain)
     # frequency range
-    ff = Fs / 2 * np.linspace(0, 1, int(LL/2+1))
+    frequency_array = sampling_frequency / 2 * np.linspace(0, 1, int(LL/2+1))
 
     # calculate FFT
     # rfft computes the fft for real inputs
-    hf = np.fft.rfft(ht)
+    frequency_domain_strain = np.fft.rfft(time_domain_strain)
 
-    # normalise to units of strain / sqrt(Hz)
-    hf = hf / Fs
+    # normalise to units of strain / Hz
+    norm_frequency_domain_strain = frequency_domain_strain / sampling_frequency
 
-    return hf, ff
+    return norm_frequency_domain_strain, frequency_array
 
 
-def infft(hf, Fs):
-    """ Inverse FFT for use in conjunction with nfft (eric.thrane@ligo.org)
+def infft(frequency_domain_strain, sampling_frequency):
+    """ Inverse FFT for use in conjunction with nfft.
 
     Parameters
-    -------
-    hf: array_like
-        single-side FFT calculated by fft_eht
-    Fs: float
-        sampling frequency
+    ----------
+    frequency_domain_strain: array_like
+        Single-sided, normalised FFT of the time-domain strain data (in units
+        of strain / Hz).
+    sampling_frequency: float
+        Sampling frequency of the data.
 
     Returns
     -------
-    array_like: time series
+    time_domain_strain: array
+        An array of the time domain strain
     """
-    # use irfft to work with positive frequencies only
-    h = np.fft.irfft(hf)
-    # undo LAL/Lasky normalisation
-    h = h*Fs
 
-    return h
+    if np.ndim(sampling_frequency) != 0:
+        raise ValueError("Sampling frequency must be integer or float")
+
+    time_domain_strain_norm = np.fft.irfft(frequency_domain_strain)
+    time_domain_strain = time_domain_strain_norm * sampling_frequency
+    return time_domain_strain
 
 
 def setup_logger(outdir=None, label=None, log_level=None, print_version=False):
diff --git a/tupak/gw/detector.py b/tupak/gw/detector.py
index 6e41d47f97c4b976e0702938e6385d382f07f08f..97a319f92538c1b8562ac2458a50fa3984c7fff3 100644
--- a/tupak/gw/detector.py
+++ b/tupak/gw/detector.py
@@ -5,8 +5,8 @@ import os
 
 import matplotlib.pyplot as plt
 import numpy as np
-from gwpy.signal import filter_design
-from scipy import signal
+import gwpy
+import scipy
 from scipy.interpolate import interp1d
 
 import tupak.gw.utils
@@ -79,7 +79,8 @@ class InterferometerSet(list):
 
 class InterferometerStrainData(object):
     """ Strain data for an interferometer """
-    def __init__(self, minimum_frequency=0, maximum_frequency=np.inf):
+    def __init__(self, minimum_frequency=0, maximum_frequency=np.inf,
+                 roll_off=0.4):
         """ Initiate an InterferometerStrainData object
 
         The initialised object contains no data, this should be added using one
@@ -88,26 +89,69 @@ class InterferometerStrainData(object):
         Parameters
         ----------
         minimum_frequency: float
-            Minimum frequency to analyse for detector.
+            Minimum frequency to analyse for detector. Default is 0.
         maximum_frequency: float
-            Maximum frequency to analyse for detector.
+            Maximum frequency to analyse for detector. Default is infinity.
+        roll_off: float
+            The roll-off (in seconds) used in the Tukey window.
 
         """
         self.minimum_frequency = minimum_frequency
         self.maximum_frequency = maximum_frequency
+        self.roll_off = roll_off
+
         self.sampling_frequency = None
         self.duration = None
         self.start_time = None
 
         self._frequency_domain_strain = None
+        self._frequency_array = None
+        self._time_domain_strain = None
+        self._time_array = None
 
-    def _calculate_frequency_array(self):
-        """ Calculate the frequency array
+    @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
 
-        Called after sampling_frequency and duration have been set.
-        """
+    @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(
-            self.sampling_frequency, self.duration)
+            sampling_frequency=self.sampling_frequency, duration=self.duration)
 
     def time_within_data(self, time):
         """ Check if time is within the data span
@@ -156,56 +200,308 @@ class InterferometerStrainData(object):
         -------
         array_like: An array of boolean values
         """
-        return (self.frequency_array > self.minimum_frequency) & (self.frequency_array < self.maximum_frequency)
+        return ((self.frequency_array > self.minimum_frequency) &
+                (self.frequency_array < self.maximum_frequency))
+
+    @property
+    def time_domain_strain(self):
+        """ The time domain strain, in units of strain """
+        if self._time_domain_strain is not None:
+            return self._time_domain_strain
+        elif self._frequency_domain_strain is not None:
+            time_domain_strain = utils.infft(
+                self.frequency_domain_strain, self.frequency_array)
+            self._time_domain_strain = time_domain_strain
+            return self._time_domain_strain
+
+        else:
+            raise ValueError("time domain strain data not yet set")
 
     @property
     def frequency_domain_strain(self):
+        """ Returns the frequency domain strain
+
+        This is the frequency domain strain normalised to units of
+        strain / Hz, obtained by a one-sided Fourier transform of the
+        time domain data, divided by the sampling frequency.
+        """
         if self._frequency_domain_strain is not None:
             return self._frequency_domain_strain * self.frequency_mask
+        elif self._time_domain_strain is not None:
+            logging.info("Generating frequency domain strain from given time "
+                         "domain strain.")
+            self.low_pass_filter()
+            self.apply_tukey_window()
+            frequency_domain_strain, _ = utils.nfft(
+                self._time_domain_strain, self.sampling_frequency)
+            self._frequency_domain_strain = frequency_domain_strain
+            return self._frequency_domain_strain
         else:
-            raise ValueError("strain_data not yet set")
+            raise ValueError("frequency domain strain data not yet set")
 
     def add_to_frequency_domain_strain(self, x):
         self._frequency_domain_strain += x
 
-    def set_from_frequency_domain_strain(
-            self, frequency_domain_strain, sampling_frequency=None,
-            duration=None, start_time=0, frequency_array=None):
-        """ Set the `Interferometer.strain_data` from a numpy array
+    def low_pass_filter(self, filter_freq=None):
+        """ Low pass filter the data """
+
+        if filter_freq is None:
+            logging.debug(
+                "Setting low pass filter_freq using given maximum frequency")
+            filter_freq = self.maximum_frequency
+
+        if 2 * filter_freq >= self.sampling_frequency:
+            logging.info(
+                "Low pass filter frequency of {}Hz requested, this is equal"
+                " or greater than the Nyquist frequency so no filter applied"
+                .format(filter_freq))
+            return
+
+        logging.debug("Applying low pass filter with filter frequency {}"
+                      .format(filter_freq))
+        bp = gwpy.signal.filter_design.lowpass(
+            filter_freq, self.sampling_frequency)
+        strain = gwpy.timeseries.TimeSeries(
+            self.time_domain_strain, sample_rate=self.sampling_frequency)
+        strain = strain.filter(bp, filtfilt=True)
+        self._time_domain_strain = strain.value
+
+    def get_tukey_window(self, N, duration):
+        alpha = 2 * self.roll_off / duration
+        window = scipy.signal.windows.tukey(N, alpha=alpha)
+        logging.debug("Generated Tukey window with alpha = {}".format(alpha))
+        return window
+
+    def apply_tukey_window(self):
+        logging.debug("Applying Tukey window with roll_off {}"
+                      .format(self.roll_off))
+        N = len(self.time_domain_strain)
+        window = self.get_tukey_window(N, duration=self.duration)
+        self._time_domain_strain *= window
+
+    def create_power_spectral_density(
+            self, fft_length, name='unknown', outdir=None):
+        """ Use the time domain strain to generate a power spectral density
+
+        This create a Tukey-windowed power spectral density and writes it to a
+        PSD file.
 
         Parameters
         ----------
-        frequency_domain_strain: array_like
-            The data to set.
+        fft_length: float
+            Duration of the analysis segment.
+        name: str
+            The name of the detector, used in storing the PSD. Defaults to
+            "unknown".
+        outdir: str
+            The output directory to write the PSD file too. If not given,
+            the PSD will not be written to file.
+
+        Returns
+        -------
+        frequency_array, psd : array_like
+            The frequencies and power spectral density array
+
+        """
+        NFFT = int(self.sampling_frequency * fft_length)
+        window = self.get_tukey_window(
+            N=NFFT, duration=fft_length)
+        strain = gwpy.timeseries.TimeSeries(
+            self.time_domain_strain, sample_rate=self.sampling_frequency)
+        psd = strain.psd(fftlength=fft_length, window=window)
+
+        if outdir:
+            psd_file = '{}/{}_PSD_{}_{}.txt'.format(
+                outdir, name, self.start_time, self.duration)
+            with open('{}'.format(psd_file), 'w+') as file:
+                for f, p in zip(psd.frequencies.value, psd.value):
+                    file.write('{} {}\n'.format(f, p))
+
+        return psd.frequencies.value, psd.value
+
+    def _check_maximum_frequency(self):
+        if 2 * self.maximum_frequency > self.sampling_frequency:
+            self.maximum_frequency = self.sampling_frequency / 2.
+
+    def _infer_time_domain_dependence(
+            self, sampling_frequency, duration, time_array):
+        """ Helper function to figure out if the time_array, or
+            sampling_frequency and duration where given
+        """
+
+        if (sampling_frequency is not None) and (duration is not None):
+            if time_array is not None:
+                raise ValueError(
+                    "You have given the sampling_frequency, duration, and "
+                    "time_array")
+            self.sampling_frequency = sampling_frequency
+            self.duration = duration
+        elif any(x is not None for x in [sampling_frequency, duration]):
+            raise ValueError(
+                "You must provide both sampling_frequency and duration")
+        elif time_array is not None:
+            self.sampling_frequency, self.duration = (
+                utils.get_sampling_frequency_and_duration_from_time_array(
+                    time_array))
+            self.time_array = np.array(time_array)
+        else:
+            raise ValueError(
+                "Insufficient information given to set time_array")
+
+    def set_from_time_domain_strain(
+            self, time_domain_strain, sampling_frequency=None, duration=None,
+            start_time=0, time_array=None):
+        """ Set the strain data from a time domain strain array
+
+        This sets the time_domain_strain attribute, the frequency_domain_strain
+        is automatically calculated after a low-pass filter and Tukey window
+        is applied.
+
+        Parameters
+        ----------
+        time_domain_strain: array_like
+            An array of the time domain strain.
         sampling_frequency: float
             The sampling frequency (in Hz).
         duration: float
             The data duration (in s).
         start_time: float
             The GPS start-time of the data.
-        frequency_array: array_like
-            The array of frequencies, if sampling_frequency and duration not
+        time_array: array_like
+            The array of times, if sampling_frequency and duration not
             given.
 
         """
-
         self.start_time = start_time
+        self._infer_time_domain_dependence(
+            sampling_frequency=sampling_frequency, duration=duration,
+            time_array=time_array)
+
+        logging.debug('Setting data using provided time_domain_strain')
+        if np.shape(time_domain_strain) == np.shape(self.time_array):
+            self._time_domain_strain = time_domain_strain
+        else:
+            raise ValueError("Data times do not match time array")
+        self._check_maximum_frequency()
+
+    def set_from_gwpy_timeseries(self, timeseries):
+        """ Set the strain data from a gwpy TimeSeries
+
+        This sets the time_domain_strain attribute, the frequency_domain_strain
+        is automatically calculated after a low-pass filter and Tukey window
+        is applied.
+
+        Parameters
+        ----------
+        timeseries: gwpy.timeseries.timeseries.TimeSeries
+
+        """
+        logging.debug('Setting data using provided gwpy TimeSeries object')
+        if type(timeseries) != gwpy.timeseries.timeseries.TimeSeries:
+            raise ValueError("Input timeseries is not a gwpy TimeSeries")
+        self.start_time = timeseries.epoch.value
+        self.sampling_frequency = timeseries.sample_rate.value
+        self.duration = timeseries.duration.value
+        self._time_domain_strain = timeseries.value
+        self._check_maximum_frequency()
+
+    def set_from_open_data(
+            self, name, start_time, duration=4, outdir='outdir', cache=True,
+            **kwargs):
+        """ Set the strain data from open LOSC data
+
+        This sets the time_domain_strain attribute, the frequency_domain_strain
+        is automatically calculated after a low-pass filter and Tukey window
+        is applied.
+
+        Parameters
+        ----------
+        name: str
+            Detector name, e.g., 'H1'.
+        start_time: float
+            Start GPS time of segment.
+        duration: float, optional
+            The total time (in seconds) to analyse. Defaults to 4s.
+        outdir: str
+            Directory where the psd files are saved
+        cache: bool, optional
+            Whether or not to store/use the acquired data.
+        **kwargs:
+            All keyword arguments are passed to
+            `gwpy.timeseries.TimeSeries.fetch_open_data()`.
+
+        """
+
+        timeseries = tupak.gw.utils.get_open_strain_data(
+            name, start_time, start_time+duration, outdir=outdir, cache=cache,
+            **kwargs)
+
+        self.set_from_gwpy_timeseries(timeseries)
+        self._check_maximum_frequency()
+
+    def set_from_csv(self, filename):
+        """ Set the strain data from a csv file
+
+        Parameters
+        ----------
+        filename: str
+            The path to the file to read in
+
+        """
+        timeseries = gwpy.timeseries.TimeSeries.read(filename, format='csv')
+        self.set_from_gwpy_timeseries(timeseries)
+        self._check_maximum_frequency()
+
+    def _infer_frequency_domain_dependence(
+            self, sampling_frequency, duration, frequency_array):
+        """ Helper function to figure out if the frequency_array, or
+            sampling_frequency and duration where given
+        """
+
         if (sampling_frequency is not None) and (duration is not None):
             if frequency_array is not None:
                 raise ValueError(
-                    "You have given the sampling_frequency, duration, and frequency_array")
+                    "You have given the sampling_frequency, duration, and "
+                    "frequency_array")
             self.sampling_frequency = sampling_frequency
             self.duration = duration
-            self._calculate_frequency_array()
         elif any(x is not None for x in [sampling_frequency, duration]):
             raise ValueError(
                 "You must provide both sampling_frequency and duration")
         elif frequency_array is not None:
             self.sampling_frequency, self.duration = (
-                utils.get_sampling_frequency_and_duration_from_frequency_array(frequency_array))
+                utils.get_sampling_frequency_and_duration_from_frequency_array(
+                    frequency_array))
             self.frequency_array = np.array(frequency_array)
         else:
-            raise ValueError("Insufficient information given to set frequency_array")
+            raise ValueError(
+                "Insufficient information given to set frequency_array")
+
+    def set_from_frequency_domain_strain(
+            self, frequency_domain_strain, sampling_frequency=None,
+            duration=None, start_time=0, frequency_array=None):
+        """ Set the `frequency_domain_strain` from a numpy array
+
+        Parameters
+        ----------
+        frequency_domain_strain: array_like
+            The data to set.
+        sampling_frequency: float
+            The sampling frequency (in Hz).
+        duration: float
+            The data duration (in s).
+        start_time: float
+            The GPS start-time of the data.
+        frequency_array: array_like
+            The array of frequencies, if sampling_frequency and duration not
+            given.
+
+        """
+
+        self.start_time = start_time
+        self._infer_frequency_domain_dependence(
+            sampling_frequency=sampling_frequency, duration=duration,
+            frequency_array=frequency_array)
 
         logging.debug('Setting data using provided frequency_domain_strain')
         if np.shape(frequency_domain_strain) == np.shape(self.frequency_array):
@@ -213,10 +509,10 @@ class InterferometerStrainData(object):
         else:
             raise ValueError("Data frequencies do not match frequency_array")
 
-    def set_from_power_spectral_density(self, power_spectral_density,
-                                        sampling_frequency, duration,
-                                        start_time=0):
-        """ Set the `Interferometer.strain_data` from a power spectral density
+    def set_from_power_spectral_density(
+            self, power_spectral_density, sampling_frequency, duration,
+            start_time=0):
+        """ Set the `frequency_domain_strain` by generating a noise realisation
 
         Parameters
         ----------
@@ -234,7 +530,6 @@ class InterferometerStrainData(object):
         self.sampling_frequency = sampling_frequency
         self.duration = duration
         self.start_time = start_time
-        self._calculate_frequency_array()
 
         logging.debug(
             'Setting data using noise realization from provided'
@@ -249,7 +544,7 @@ class InterferometerStrainData(object):
             raise ValueError("Data frequencies do not match frequency_array")
 
     def set_from_zero_noise(self, sampling_frequency, duration, start_time=0):
-        """ Set the `Interferometer.strain_data` to zero noise
+        """ Set the `frequency_domain_strain` to zero noise
 
         Parameters
         ----------
@@ -265,15 +560,15 @@ class InterferometerStrainData(object):
         self.sampling_frequency = sampling_frequency
         self.duration = duration
         self.start_time = start_time
-        self._calculate_frequency_array()
 
         logging.debug('Setting zero noise data')
-        self._frequency_domain_strain = np.zeros_like(self.frequency_array) * (1 + 1j)
+        self._frequency_domain_strain = np.zeros_like(self.frequency_array,
+                                                      dtype=np.complex)
 
-    def set_from_frame_file(self, frame_file, sampling_frequency,
-                            duration, start_time=0, channel_name=None,
-                            buffer_time=1, filter_freq=None, alpha=0.25):
-        """ Set the `Interferometer.strain_data` from a frame file
+    def set_from_frame_file(
+            self, frame_file, sampling_frequency, duration, start_time=0,
+            channel_name=None, buffer_time=1):
+        """ Set the `frequency_domain_strain` from a frame fiile
 
         Parameters
         ----------
@@ -290,23 +585,12 @@ class InterferometerStrainData(object):
         buffer_time: float
             Read in data with `start_time-buffer_time` and
             `start_time+duration+buffer_time`
-        alpha: float
-            The tukey window shape parameter passed to `scipy.signal.tukey`.
-        filter_freq: float
-            Low pass filter frequency. If None, defaults to the maximum
-            frequency given to InterferometerStrainData.
 
         """
 
-        if filter_freq is None:
-            logging.debug(
-                "Setting low pass filter_freq using given maximum frequency")
-            filter_freq = self.maximum_frequency
-
         self.sampling_frequency = sampling_frequency
         self.duration = duration
         self.start_time = start_time
-        self._calculate_frequency_array()
 
         logging.info('Reading data from frame')
         strain = tupak.gw.utils.read_frame_file(
@@ -314,12 +598,7 @@ class InterferometerStrainData(object):
             buffer_time=buffer_time, channel=channel_name,
             resample=sampling_frequency)
 
-        frequency_domain_strain, frequencies = tupak.gw.utils.process_strain_data(
-            strain, filter_freq=filter_freq, alpha=alpha)
-        if np.array_equal(frequencies, self.frequency_array):
-            self._frequency_domain_strain = frequency_domain_strain
-        else:
-            raise ValueError("Data frequencies do not match frequency_array")
+        self.set_from_gwpy_timeseries(strain)
 
 
 class Interferometer(object):
@@ -378,6 +657,14 @@ class Interferometer(object):
             minimum_frequency=minimum_frequency,
             maximum_frequency=maximum_frequency)
 
+    @property
+    def minimum_frequency(self):
+        return self.strain_data.minimum_frequency
+
+    @property
+    def maximum_frequency(self):
+        return self.strain_data.maximum_frequency
+
     @property
     def strain_data(self):
         """ A tupak.gw.detector.InterferometerStrainData instance """
@@ -385,7 +672,16 @@ class Interferometer(object):
 
     @strain_data.setter
     def strain_data(self, strain_data):
-        """ Set the strain_data """
+        """ Set the strain_data
+
+        This sets the Interferometer.strain_data equal to the provided
+        strain_data. This will overide the minimum_frequency and
+        maximum_frequency of the provided strain_data object with those of
+        the Interferometer object.
+        """
+        strain_data.minimum_frequency = self.minimum_frequency
+        strain_data.maximum_frequency = self.maximum_frequency
+
         self._strain_data = strain_data
 
     def set_strain_data_from_frequency_domain_strain(
@@ -437,7 +733,7 @@ class Interferometer(object):
 
     def set_strain_data_from_frame_file(
             self, frame_file, sampling_frequency, duration, start_time=0,
-            channel_name=None, buffer_time=1, alpha=0.25, filter_freq=None):
+            channel_name=None, buffer_time=1):
         """ Set the `Interferometer.strain_data` from a frame file
 
         Parameters
@@ -455,18 +751,23 @@ class Interferometer(object):
         buffer_time: float
             Read in data with `start_time-buffer_time` and
             `start_time+duration+buffer_time`
-        alpha: float
-            The tukey window shape parameter passed to `scipy.signal.tukey`.
-        filter_freq: float
-            Low pass filter frequency. If None, defaults to the maximum
-            frequency given to InterferometerStrainData.
 
         """
         self.strain_data.set_from_frame_file(
             frame_file=frame_file, sampling_frequency=sampling_frequency,
             duration=duration, start_time=start_time,
-            channel_name=channel_name, buffer_time=buffer_time,
-            alpha=alpha, filter_freq=filter_freq)
+            channel_name=channel_name, buffer_time=buffer_time)
+
+    def set_strain_data_from_csv(self, filename):
+        """ Set the `Interferometer.strain_data` from a csv file
+
+        Parameters
+        ----------
+        filename: str
+            The path to the file to read in
+
+        """
+        self.strain_data.set_from_csv(filename)
 
     def set_strain_data_from_zero_noise(
             self, sampling_frequency, duration, start_time=0):
@@ -884,6 +1185,7 @@ class Interferometer(object):
 
     @property
     def frequency_domain_strain(self):
+        """ The frequency domain strain in units of strain / Hz """
         return self.strain_data.frequency_domain_strain
 
     def time_delay_from_geocenter(self, ra, dec, time):
@@ -981,46 +1283,31 @@ class Interferometer(object):
 
 class PowerSpectralDensity(object):
 
-    def __init__(self, asd_file=None, psd_file='aLIGO_ZERO_DET_high_P_psd.txt', frame_file=None, asd_array=None,
-                 psd_array=None, frequencies=None, start_time=0,
-                 psd_duration=1024, psd_offset=16, channel_name=None, filter_freq=1024, alpha=0.25, fft_length=4):
+    def __init__(self, **kwargs):
         """
         Instantiate a new PowerSpectralDensity object.
 
-        Only one of the asd_file or psd_file needs to be specified.
-        If multiple are given, the first will be used.
+        If called with no argument, `PowerSpectralDensity()` will return an
+        empty instance which can be filled with one of the `set_from` methods.
+        You can also initialise a new PowerSpectralDensity object giving the
+        arguments of any `set_from` method and an attempt will be made to use
+        this information to load/create the power spectral density.
 
-        Parameters
+        Example
         -------
-        asd_file: str, optional
-            File containing amplitude spectral density, format 'f h_f'
-        psd_file: str, optional
-            File containing power spectral density, format 'f h_f'
-        frame_file: str, optional
-            Frame file to read data from.
-        asd_array: array_like, optional
-            List of amplitude spectral density values corresponding to frequency_array,
-            requires frequency_array to be specified.
-        psd_array: array_like, optional
-            List of power spectral density values corresponding to frequency_array,
-            requires frequency_array to be specified.
-        frequencies: array_like, optional
-            List of frequency values corresponding to asd_array/psd_array,
-        start_time: float, optional
-            Beginning of segment to analyse.
-        psd_duration: float, optional
-            Duration of data to generate PSD from.
-        psd_offset: float, optional
-            Offset of data from beginning of analysed segment.
-        channel_name: str, optional
-            Name of channel to use to generate PSD.
-        alpha: float, optional
-            Parameter for Tukey window.
-        fft_length: float, optional
-            Number of seconds in a single fft.
+        Using the `set_from` method directly (here `psd_file` is a string
+        containing the path to the file to load):
+        >>> power_spectral_density = PowerSpectralDensity()
+        >>> power_spectral_density.set_from_power_spectral_density_file(psd_file)
+
+        Alternatively (and equivalently) setting the psd_file directly:
+        >>> power_spectral_density = PowerSpectralDensity(psd_file=psd_file)
+
+        Note: for the "direct" method to work, you must provide the input
+        as a keyword argument as above.
 
         Attributes
-        -------
+        ----------
         amplitude_spectral_density: array_like
             Array representation of the ASD
         amplitude_spectral_density_file: str
@@ -1033,6 +1320,7 @@ class PowerSpectralDensity(object):
             Name of the PSD file
         power_spectral_density_interpolated: scipy.interpolated.interp1d
             Interpolated function of the PSD
+
         """
         self.__power_spectral_density = None
         self.__amplitude_spectral_density = None
@@ -1040,52 +1328,112 @@ class PowerSpectralDensity(object):
         self.frequencies = []
         self.power_spectral_density_interpolated = None
 
-        if asd_file is not None:
-            self.amplitude_spectral_density_file = asd_file
-            self.import_amplitude_spectral_density()
-            if min(self.amplitude_spectral_density) < 1e-30:
-                logging.warning("You specified an amplitude spectral density file.")
-                logging.warning("{} WARNING {}".format("*" * 30, "*" * 30))
-                logging.warning("The minimum of the provided curve is {:.2e}.".format(
-                    min(self.amplitude_spectral_density)))
-                logging.warning("You may have intended to provide this as a power spectral density.")
-        elif frame_file is not None:
-            strain = tupak.gw.utils.read_frame_file(frame_file, t1=start_time - psd_duration - psd_offset,
-                                                    t2=start_time - psd_offset, channel=channel_name)
-            sampling_frequency = int(strain.sample_rate.value)
-
-            # Low pass filter
-            bp = filter_design.lowpass(filter_freq, strain.sample_rate)
-            strain = strain.filter(bp, filtfilt=True)
-            strain = strain.crop(*strain.span.contract(1))
-
-            # Create and save PSDs
-            nfft = int(sampling_frequency * fft_length)
-            window = signal.windows.tukey(nfft, alpha=alpha)
-            psd = strain.psd(fftlength=fft_length, window=window)
-            self.frequencies = psd.frequencies
-            self.power_spectral_density = psd.value
-        elif frequencies is not None:
-            self.frequencies = frequencies
-            if asd_array is not None:
-                self.amplitude_spectral_density = asd_array
-            elif psd_array is not None:
-                self.power_spectral_density = psd_array
-        else:
-            if psd_file is None:
-                logging.info("No power spectral density provided, using aLIGO, zero detuning, high power.")
-            self.power_spectral_density_file = psd_file
-            self.import_power_spectral_density()
-            if min(self.power_spectral_density) > 1e-30:
-                logging.warning("You specified a power spectral density file.")
-                logging.warning("{} WARNING {}".format("*" * 30, "*" * 30))
-                logging.warning("The minimum of the provided curve is {:.2e}.".format(
-                    min(self.power_spectral_density)))
-                logging.warning("You may have intended to provide this as an amplitude spectral density.")
+        for key in kwargs:
+            try:
+                expanded_key = (key.replace('psd', 'power_spectral_density')
+                                .replace('asd', 'amplitude_spectral_density'))
+                m = getattr(self, 'set_from_{}'.format(expanded_key))
+                m(**kwargs)
+            except AttributeError:
+                logging.info("Tried setting PSD from init kwarg {} and failed"
+                             .format(key))
+
+    def set_from_amplitude_spectral_density_file(self, asd_file):
+        """ Set the amplitude spectral density from a given file
+
+        Parameters
+        ----------
+        asd_file: str
+            File containing amplitude spectral density, format 'f h_f'
+
+        """
+
+        self.amplitude_spectral_density_file = asd_file
+        self.import_amplitude_spectral_density()
+        if min(self.amplitude_spectral_density) < 1e-30:
+            logging.warning("You specified an amplitude spectral density file.")
+            logging.warning("{} WARNING {}".format("*" * 30, "*" * 30))
+            logging.warning("The minimum of the provided curve is {:.2e}.".format(
+                min(self.amplitude_spectral_density)))
+            logging.warning(
+                "You may have intended to provide this as a power spectral density.")
+
+    def set_from_power_spectral_density_file(self, psd_file):
+        """ Set the power spectral density from a given file
+
+        Parameters
+        ----------
+        psd_file: str, optional
+            File containing power spectral density, format 'f h_f'
+
+        """
+
+        self.power_spectral_density_file = psd_file
+        self.import_power_spectral_density()
+        if min(self.power_spectral_density) > 1e-30:
+            logging.warning("You specified a power spectral density file.")
+            logging.warning("{} WARNING {}".format("*" * 30, "*" * 30))
+            logging.warning("The minimum of the provided curve is {:.2e}.".format(
+                min(self.power_spectral_density)))
+            logging.warning(
+                "You may have intended to provide this as an amplitude spectral density.")
+
+    def set_from_frame_file(self, frame_file, psd_start_time, psd_duration,
+                            fft_length=4, filter_freq=1024, alpha=0.25,
+                            channel=None):
+        """ Generate power spectral density from a frame file
+
+        Parameters
+        ----------
+        frame_file: str, optional
+            Frame file to read data from.
+        psd_start_time: float
+            Beginning of segment to analyse.
+        psd_duration: float, optional
+            Duration of data (in seconds) to generate PSD from.
+        fft_length: float, optional
+            Number of seconds in a single fft.
+        filter_freq: float
+            Low pass filter frequency
+        alpha: float, optional
+            Parameter for Tukey window.
+        channel_name: str, optional
+            Name of channel to use to generate PSD.
+
+        """
+
+        strain = tupak.gw.detector.InterferometerStrainData()
+        strain.set_from_frame_file(
+            frame_file, t1=psd_start_time, t2=psd_start_time+psd_duration,
+            channel=channel)
+
+        strain.low_pass_filter(filter_freq)
+        f, psd = strain.create_power_spectral_density(fft_length=fft_length)
+        self.frequencies = f
+        self.power_spectral_density = psd
+
+    def set_from_amplitude_spectral_density_array(self, frequencies,
+                                                  asd_array):
+        self.frequencies = frequencies
+        self.amplitude_spectral_density = asd_array
+
+    def set_from_power_spectral_density_array(self, frequencies, psd_array):
+        self.frequencies = frequencies
+        self.power_spectral_density = psd_array
+
+    def set_from_aLIGO(self):
+        psd_file = 'aLIGO_ZERO_DET_high_P_psd.txt'
+        logging.info("No power spectral density provided, using aLIGO,"
+                     "zero detuning, high power.")
+        self.set_from_power_spectral_density_file(psd_file)
 
     @property
     def power_spectral_density(self):
-        return self.__power_spectral_density
+        if self.__power_spectral_density is not None:
+            return self.__power_spectral_density
+        else:
+            self.set_to_aLIGO()
+            return self.__power_spectral_density
 
     @power_spectral_density.setter
     def power_spectral_density(self, power_spectral_density):
@@ -1107,40 +1455,50 @@ class PowerSpectralDensity(object):
 
     def import_amplitude_spectral_density(self):
         """
-        Automagically load one of the amplitude spectral density curves contained in the noise_curves directory.
+        Automagically load one of the amplitude spectral density curves
+        contained in the noise_curves directory.
 
         Test if the file contains a path (i.e., contains '/').
         If not assume the file is in the default directory.
         """
+
         if '/' not in self.amplitude_spectral_density_file:
-            self.amplitude_spectral_density_file = os.path.join(os.path.dirname(__file__), 'noise_curves',
-                                                                self.amplitude_spectral_density_file)
-        self.frequencies, self.amplitude_spectral_density = np.genfromtxt(self.amplitude_spectral_density_file).T
+            self.amplitude_spectral_density_file = os.path.join(
+                os.path.dirname(__file__), 'noise_curves',
+                self.amplitude_spectral_density_file)
+
+        self.frequencies, self.amplitude_spectral_density = np.genfromtxt(
+            self.amplitude_spectral_density_file).T
 
     def import_power_spectral_density(self):
         """
-        Automagically load one of the power spectral density curves contained in the noise_curves directory.
+        Automagically load one of the power spectral density curves contained
+        in the noise_curves directory.
 
         Test if the file contains a path (i.e., contains '/').
         If not assume the file is in the default directory.
         """
         if '/' not in self.power_spectral_density_file:
-            self.power_spectral_density_file = os.path.join(os.path.dirname(__file__), 'noise_curves',
-                                                            self.power_spectral_density_file)
-        self.frequencies, self.power_spectral_density = np.genfromtxt(self.power_spectral_density_file).T
+            self.power_spectral_density_file = os.path.join(
+                os.path.dirname(__file__), 'noise_curves',
+                self.power_spectral_density_file)
+        self.frequencies, self.power_spectral_density = np.genfromtxt(
+                self.power_spectral_density_file).T
 
     def _check_frequency_array_matches_density_array(self, density_array):
-        """Check if the provided frequency and spectral density arrays match."""
+        """Check the provided frequency and spectral density arrays match."""
         try:
             self.frequencies - density_array
         except ValueError as e:
             raise(e, 'Provided spectral density does not match frequency array. Not updating.')
 
     def _interpolate_power_spectral_density(self):
-        """Interpolate the loaded PSD so it can be resampled for arbitrary frequency arrays."""
-        self.power_spectral_density_interpolated = interp1d(self.frequencies, self.power_spectral_density,
-                                                            bounds_error=False,
-                                                            fill_value=np.inf)
+        """Interpolate the loaded power spectral density so it can be resampled
+           for arbitrary frequency arrays.
+        """
+        self.power_spectral_density_interpolated = interp1d(
+            self.frequencies, self.power_spectral_density, bounds_error=False,
+            fill_value=np.inf)
 
     def get_noise_realisation(self, sampling_frequency, duration):
         """
@@ -1222,8 +1580,8 @@ def load_interferometer(filename):
 
 
 def get_interferometer_with_open_data(
-        name, trigger_time, time_duration=4, start_time=None, alpha=0.25, psd_offset=-1024,
-        psd_duration=100, cache=True, outdir='outdir', label=None, plot=True, filter_freq=1024,
+        name, trigger_time, time_duration=4, start_time=None, roll_off=0.4, psd_offset=-1024,
+        psd_duration=100, cache=True, outdir='outdir', label=None, plot=True, filter_freq=None,
         raw_data_file=None, **kwargs):
     """
     Helper function to obtain an Interferometer instance with appropriate
@@ -1241,8 +1599,8 @@ def get_interferometer_with_open_data(
     start_time: float, optional
         Beginning of the segment, if None, the trigger is placed 2s before the end
         of the segment.
-    alpha: float, optional
-        The tukey window shape parameter passed to `scipy.signal.tukey`.
+    roll_off: float
+        The roll-off (in seconds) used in the Tukey window.
     psd_offset, psd_duration: float
         The power spectral density (psd) is estimated using data from
         `center_time+psd_offset` to `center_time+psd_offset + psd_duration`.
@@ -1278,50 +1636,25 @@ def get_interferometer_with_open_data(
     if start_time is None:
         start_time = trigger_time + 2 - time_duration
 
-    strain = tupak.gw.utils.get_open_strain_data(
-        name, start_time - 1, start_time + time_duration + 1, outdir=outdir, cache=cache,
-        raw_data_file=raw_data_file, **kwargs)
-
-    strain_psd = tupak.gw.utils.get_open_strain_data(
-        name, start_time + time_duration + psd_offset,
-        start_time + time_duration + psd_offset + psd_duration,
-        raw_data_file=raw_data_file,
+    strain = InterferometerStrainData(roll_off=roll_off)
+    strain.set_from_open_data(
+        name=name, start_time=start_time, duration=time_duration,
         outdir=outdir, cache=cache, **kwargs)
 
-    sampling_frequency = int(strain.sample_rate.value)
-
+    strain_psd = InterferometerStrainData(roll_off=roll_off)
+    strain_psd.set_from_open_data(
+        name=name, start_time=start_time + time_duration + psd_offset,
+        duration=psd_duration, outdir=outdir, cache=cache, **kwargs)
     # Low pass filter
-    bp = filter_design.lowpass(filter_freq, strain.sample_rate)
-    strain = strain.filter(bp, filtfilt=True)
-    strain = strain.crop(*strain.span.contract(1))
-    strain_psd = strain_psd.filter(bp, filtfilt=True)
-    strain_psd = strain_psd.crop(*strain_psd.span.contract(1))
-
+    strain_psd.low_pass_filter(filter_freq)
     # Create and save PSDs
-    NFFT = int(sampling_frequency * time_duration)
-    window = signal.windows.tukey(NFFT, alpha=alpha)
-    psd = strain_psd.psd(fftlength=time_duration, window=window)
-    psd_file = '{}/{}_PSD_{}_{}.txt'.format(
-        outdir, name, start_time + time_duration + psd_offset, psd_duration)
-    with open('{}'.format(psd_file), 'w+') as file:
-        for f, p in zip(psd.frequencies.value, psd.value):
-            file.write('{} {}\n'.format(f, p))
-
-    time_series = strain.times.value
-    time_duration = time_series[-1] - time_series[0]
-
-    # Apply Tukey window
-    N = len(time_series)
-    strain = strain * signal.windows.tukey(N, alpha=alpha)
+    psd_frequencies, psd_array = strain_psd.create_power_spectral_density(
+        name=name, outdir=outdir, fft_length=strain.duration)
 
     interferometer = get_empty_interferometer(name)
     interferometer.power_spectral_density = PowerSpectralDensity(
-        psd_file=psd_file)
-    interferometer.set_strain_data_from_frequency_domain_strain(
-        frequency_domain_strain=utils.nfft(
-            strain.value, sampling_frequency)[0],
-        sampling_frequency=sampling_frequency, duration=time_duration,
-        start_time=strain.epoch.value)
+        psd_array=psd_array, frequencies=psd_frequencies)
+    interferometer.strain_data = strain
 
     if plot:
         interferometer.plot_data(outdir=outdir, label=label)
@@ -1338,6 +1671,9 @@ def get_interferometer_with_fake_noise_and_injection(
     Helper function to obtain an Interferometer instance with appropriate
     power spectral density and data, given an center_time.
 
+    Note: by default this generates an Interferometer with a power spectral
+    density based on advanced LIGO.
+
     Parameters
     ----------
     name: str
@@ -1382,6 +1718,7 @@ def get_interferometer_with_fake_noise_and_injection(
         start_time = injection_parameters['geocent_time'] + 2 - time_duration
 
     interferometer = get_empty_interferometer(name)
+    interferometer.power_spectral_density.set_from_aLIGO()
     if zero_noise:
         interferometer.set_strain_data_from_zero_noise(
             sampling_frequency=sampling_frequency, duration=time_duration,
@@ -1409,9 +1746,9 @@ def get_interferometer_with_fake_noise_and_injection(
 
 
 def get_event_data(
-        event, interferometer_names=None, time_duration=4, alpha=0.25,
+        event, interferometer_names=None, time_duration=4, roll_off=0.4,
         psd_offset=-1024, psd_duration=100, cache=True, outdir='outdir',
-        label=None, plot=True, filter_freq=1024, raw_data_file=None, **kwargs):
+        label=None, plot=True, filter_freq=None, raw_data_file=None, **kwargs):
     """
     Get open data for a specified event.
 
@@ -1425,8 +1762,8 @@ def get_event_data(
         If None will look for data in 'H1', 'V1', 'L1'
     time_duration: float
         Time duration to search for.
-    alpha: float
-        The tukey window shape parameter passed to `scipy.signal.tukey`.
+    roll_off: float
+        The roll-off (in seconds) used in the Tukey window.
     psd_offset, psd_duration: float
         The power spectral density (psd) is estimated using data from
         `center_time+psd_offset` to `center_time+psd_offset + psd_duration`.
@@ -1463,11 +1800,12 @@ def get_event_data(
     for name in interferometer_names:
         try:
             interferometers.append(get_interferometer_with_open_data(
-                name, trigger_time=event_time, time_duration=time_duration, alpha=alpha,
+                name, trigger_time=event_time, time_duration=time_duration, roll_off=roll_off,
                 psd_offset=psd_offset, psd_duration=psd_duration, cache=cache,
                 outdir=outdir, label=label, plot=plot, filter_freq=filter_freq,
                 raw_data_file=raw_data_file, **kwargs))
-        except ValueError:
+        except ValueError as e:
+            logging.debug("Error raised {}".format(e))
             logging.warning('No data found for {}.'.format(name))
 
     return InterferometerSet(interferometers)
diff --git a/tupak/gw/utils.py b/tupak/gw/utils.py
index 0d1ad69f8b001668a97db31dc737328ff282157a..88e28ad4ce48079bd89d2d8ed2ec653ebf64427b 100644
--- a/tupak/gw/utils.py
+++ b/tupak/gw/utils.py
@@ -291,7 +291,7 @@ def get_event_time(event):
 
 
 def get_open_strain_data(
-        name, t1, t2, outdir, cache=False, raw_data_file=None, **kwargs):
+        name, t1, t2, outdir, cache=False, buffer_time=0, **kwargs):
     """ A function which accesses the open strain data
 
     This uses `gwpy` to download the open data and then saves a cached copy for
@@ -307,9 +307,10 @@ def get_open_strain_data(
         The output directory to place data in
     cache: bool
         If true, cache the data
+    buffer_time: float
+        Time to add to the begining and end of the segment.
     **kwargs:
         Passed to `gwpy.timeseries.TimeSeries.fetch_open_data`
-    raw_data_file
 
     Returns
     -----------
@@ -317,19 +318,18 @@ def get_open_strain_data(
 
     """
     filename = '{}/{}_{}_{}.txt'.format(outdir, name, t1, t2)
-    if raw_data_file is not None:
-        logging.info('Using raw_data_file {}'.format(raw_data_file))
-        strain = TimeSeries.read(raw_data_file)
-        if (t1 > strain.times[0].value) and (t2 < strain.times[-1].value):
-            logging.info('Using supplied raw data file')
-            strain = strain.crop(t1, t2)
-        else:
-            raise ValueError('Supplied file does not contain requested data')
-    elif os.path.isfile(filename) and cache:
+
+    if buffer_time < 0:
+        raise ValueError("buffer_time < 0")
+    t1 = t1 - buffer_time
+    t2 = t2 + buffer_time
+
+    if os.path.isfile(filename) and cache:
         logging.info('Using cached data from {}'.format(filename))
         strain = TimeSeries.read(filename)
     else:
-        logging.info('Fetching open data ...')
+        logging.info('Fetching open data from {} to {} with buffer time {}'
+                     .format(t1, t2, buffer_time))
         strain = TimeSeries.fetch_open_data(name, t1, t2, **kwargs)
         logging.info('Saving data to {}'.format(filename))
         strain.write(filename)