4.2.2. Scipy examples

Scipy provides lots of useful functions for scientific computing, such as analyzing signals. This forms the starting point for implementing signal processing, which is a very common topic in electronic engineering. For example, as we had the first part of the lab, most things that we measure in the lab are subject to interference and noise from other sources. Signal processing can help us filter and remove this noise.

4.2.2.1. Signal filtering example

  1. In your Lab E folder you will see there is also a folder called data. Here we’ve put some files that contain pre-saved data for us. For example, this might be a recording we did in the lab using an oscilloscope or data acquisition system.

    We’ve stored the data in Matlab format, with an extension .mat. This is a very dense data format that lets us store lots of data in a relatively small file size.

    Scipy has a function that can read these .mat files for us, called scipy.io.loadmat(). You can read its documentation online.

  2. Start a new Python file in your Lab E src folder. Call it something like signal_filtering.py. Write a function that loads the file data/signal_data.mat, and extracts the time vector and signal vector from it, and then plots it.

    • The variable names in the file are t for the time vector, and v for the signal vector.

    • You might need to think about where the data file is located relative to your code file. In the solution below we’ve used the pathlib library to find this automatically, but you can just type in the address by hand if you prefer.

    • Think about what type of variables you want to store your output in. scipy.io.loadmat() will load the data into a dictionary, so you probably want to extract the variables from this dictionary and put them in numpy arrays.

    Your plot should look similar to the below, where we’ve zoomed-in a bit.

    Sine wave with noise plotted with plotly

    Screenshot of a web browser, software from Brave. See course copyright statement.

    This is a 10 Hz sine wave, corrupted by some high frequency noise. It’s the same as the signal we made the first part of the lab by adding random noise to a 10 Hz sine wave, we’ve now just loaded it in from a file.

  3. Scipy contains functions for filtering this noise out. Add the function below to your code, edit the rest of the code to call this function, and plot the filtered signal. You’ll need to work out the correct parameters to pass to filter_data().

    def generate_filter_coefficients(cutoff, fs, order, ftype):
        """Calculate filter coefficients for a Butterworth filter"""
        nyq = 0.5 * fs  # expects the cutoff frequency to be given in Hz, it then scales this to be in the range 0 to 1, where 1 is half the sampling frequency
        normalized_cutoff = cutoff / nyq
        b, a = signal.butter(order, normalized_cutoff, btype=ftype, analog=False)
        return b, a
    
    
    def filter_data(data, cutoff, fs, order, ftype):
        """Filter the data"""
        b, a = generate_filter_coefficients(cutoff, fs, order, ftype)
        signal_filtered = signal.filtfilt(b, a, data)
        return signal_filtered
    

    This is implementing a Butterworth filter, which you will have learnt about in your electronics courses. It attenuates frequencies higher than the cutoff.

  4. Assuming you’ve implemented the filtering as in the solution above, the sine wave still looks quite noisy. By using different filtering functions as given in the scipy signal documentation, or otherwise, try and clean the signal more to make it look like a pure sine wave.

4.2.2.2. Signal detection example

  1. Your Lab E data folder also contains a file called ecg.mat, which contains vectors time and ecg. Write a Python script that loads and plots this data. The start of the plot will look like the below.

    An ECG trace plotted with plotly

    Screenshot of a web browser, software from Brave. See course copyright statement.

  2. In the ECG signal there are some clear, sharp, peaks. These correspond to heart beats. Write code to automatically detect these peaks using the signal.find_peaks() function. As such, calculate the average heart rate in beats per minute (bpm) over the duration of the recording.

  3. Check your code in to Git before proceeding.

4.2.2.3. Further (optional) tasks

  1. Usually we don’t want to estimate the heart rate from the entire signal. This would mean waiting a long time to get all of the data.

    It’s common (in wearables) that we estimate heart rate using 8 second windows of data. Every 2 s this window moves on, so there’s 6 seconds of overlap between each window. This means that every 2 s we get a new estimate of heart rate, based on the last 8 seconds of data. We can then get a plot of heart rate over time.

    Within each window of data we calculate the heart rate with the equation

    \[hr = 60 \times \frac{n_{beats} - 1 }{t_{last} - t_{first}}\]

    where \(n_{beats}\) is the number of detected beats in the window, and \(t_{last}\) and \(t_{first}\) are the times of the last and first detected beats in the window respectively.

    Write code to implement this sliding window heart rate estimation, and plot the resulting heart rate over time.

  2. In ecg.mat there is another variable called ecg_noise, which is a noisier version of the ECG signal. Try and detect the peaks in this noisier signal. You might need to do some filtering first to clean up the signal before peak detection will work well, and/or make your peak detection function more robust.