Source code for neurokit2.rsp.rsp_findpeaks

# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
import scipy.signal


[docs] def rsp_findpeaks( rsp_cleaned, sampling_rate=1000, method="khodadad2018", amplitude_min=0.3, peak_distance=0.8, peak_prominence=0.5, ): """**Extract extrema in a respiration (RSP) signal** Low-level function used by :func:`.rsp_peaks` to identify inhalation and exhalation onsets (troughs and peaks respectively) in a preprocessed respiration signal using different sets of parameters. See :func:`.rsp_peaks` for details. Parameters ---------- rsp_cleaned : Union[list, np.array, pd.Series] The cleaned respiration channel as returned by :func:`.rsp_clean`. sampling_rate : int The sampling frequency of :func:`.rsp_cleaned` (in Hz, i.e., samples/second). method : str The processing pipeline to apply. Can be one of ``"khodadad2018"`` (default), ``"scipy"`` or ``"biosppy"``. amplitude_min : float Only applies if method is ``"khodadad2018"``. Extrema that have a vertical distance smaller than(outlier_threshold * average vertical distance) to any direct neighbour are removed as false positive outliers. I.e., outlier_threshold should be a float with positive sign (the default is 0.3). Larger values of outlier_threshold correspond to more conservative thresholds (i.e., more extrema removed as outliers). peak_distance: float Only applies if method is ``"scipy"``. Minimal distance between peaks. Default is 0.8 seconds. peak_prominence: float Only applies if method is ``"scipy"``. Minimal prominence between peaks. Default is 0.5. Returns ------- info : dict A dictionary containing additional information, in this case the samples at which inhalation onsets and exhalation onsets occur, accessible with the keys ``"RSP_Troughs"`` and ``"RSP_Peaks"``, respectively. See Also -------- rsp_clean, rsp_fixpeaks, rsp_peaks, signal_rate, rsp_amplitude, rsp_process, rsp_plot Examples -------- .. ipython:: python import neurokit2 as nk rsp = nk.rsp_simulate(duration=30, respiratory_rate=15) cleaned = nk.rsp_clean(rsp, sampling_rate=1000) info = nk.rsp_findpeaks(cleaned) @savefig p_rsp_findpeaks1.png scale=100% nk.events_plot([info["RSP_Peaks"], info["RSP_Troughs"]], cleaned) @suppress plt.close() """ # Try retrieving correct column if isinstance(rsp_cleaned, pd.DataFrame): try: rsp_cleaned = rsp_cleaned["RSP_Clean"] except NameError: try: rsp_cleaned = rsp_cleaned["RSP_Raw"] except NameError: rsp_cleaned = rsp_cleaned["RSP"] cleaned = np.array(rsp_cleaned) # Find peaks method = method.lower() # remove capitalised letters if method in ["khodadad", "khodadad2018"]: info = _rsp_findpeaks_khodadad(cleaned, amplitude_min=amplitude_min) elif method == "biosppy": info = _rsp_findpeaks_biosppy(cleaned, sampling_rate=sampling_rate) elif method == "scipy": info = _rsp_findpeaks_scipy( cleaned, sampling_rate=sampling_rate, peak_distance=peak_distance, peak_prominence=peak_prominence, ) else: raise ValueError( "NeuroKit error: rsp_findpeaks(): 'method' should be one of 'khodadad2018', 'scipy' or 'biosppy'." ) return info
# ============================================================================= # Methods # ============================================================================= def _rsp_findpeaks_biosppy(rsp_cleaned, sampling_rate): """https://github.com/PIA-Group/BioSPPy/blob/master/biosppy/signals/resp.py""" extrema = _rsp_findpeaks_extrema(rsp_cleaned) extrema, amplitudes = _rsp_findpeaks_outliers(rsp_cleaned, extrema, amplitude_min=0) peaks, troughs = _rsp_findpeaks_sanitize(extrema, amplitudes) # Apply minimum period outlier-criterion (exclude inter-breath-intervals # that produce breathing rate larger than 35 breaths per minute. outlier_idcs = np.where((np.diff(peaks) / sampling_rate) < 1.7)[0] peaks = np.delete(peaks, outlier_idcs) troughs = np.delete(troughs, outlier_idcs) info = {"RSP_Peaks": peaks, "RSP_Troughs": troughs} return info def _rsp_findpeaks_khodadad(rsp_cleaned, amplitude_min=0.3): """https://iopscience.iop.org/article/10.1088/1361-6579/aad7e6/meta""" extrema = _rsp_findpeaks_extrema(rsp_cleaned) extrema, amplitudes = _rsp_findpeaks_outliers(rsp_cleaned, extrema, amplitude_min=amplitude_min) peaks, troughs = _rsp_findpeaks_sanitize(extrema, amplitudes) info = {"RSP_Peaks": peaks, "RSP_Troughs": troughs} return info def _rsp_findpeaks_scipy(rsp_cleaned, sampling_rate, peak_distance=0.8, peak_prominence=0.5): """https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.find_peaks.html""" peak_distance = sampling_rate * peak_distance peaks, _ = scipy.signal.find_peaks( rsp_cleaned, distance=peak_distance, prominence=peak_prominence ) troughs, _ = scipy.signal.find_peaks( -rsp_cleaned, distance=peak_distance, prominence=peak_prominence ) # Combine peaks and troughs and sort them. extrema = np.sort(np.concatenate((peaks, troughs))) # Sanitize. extrema, amplitudes = _rsp_findpeaks_outliers(rsp_cleaned, extrema, amplitude_min=0) peaks, troughs = _rsp_findpeaks_sanitize(extrema, amplitudes) info = {"RSP_Peaks": peaks, "RSP_Troughs": troughs} return info # ============================================================================= # Internals # ============================================================================= def _rsp_findpeaks_extrema(rsp_cleaned): # Detect zero crossings (note that these are zero crossings in the raw # signal, not in its gradient). greater = rsp_cleaned > 0 smaller = rsp_cleaned < 0 risex = np.where(np.bitwise_and(smaller[:-1], greater[1:]))[0] fallx = np.where(np.bitwise_and(greater[:-1], smaller[1:]))[0] if risex[0] < fallx[0]: startx = "rise" elif fallx[0] < risex[0]: startx = "fall" allx = np.concatenate((risex, fallx)) allx.sort(kind="mergesort") # Find extrema by searching minima between falling zero crossing and # rising zero crossing, and searching maxima between rising zero # crossing and falling zero crossing. extrema = [] for i in range(len(allx) - 1): # Determine whether to search for minimum or maximum. if startx == "rise": if (i + 1) % 2 != 0: argextreme = np.argmax else: argextreme = np.argmin elif startx == "fall": if (i + 1) % 2 != 0: argextreme = np.argmin else: argextreme = np.argmax # Get the two zero crossings between which the extreme will be # searched. beg = allx[i] end = allx[i + 1] extreme = argextreme(rsp_cleaned[beg:end]) extrema.append(beg + extreme) extrema = np.asarray(extrema) return extrema def _rsp_findpeaks_outliers(rsp_cleaned, extrema, amplitude_min=0.3): # Only consider those extrema that have a minimum vertical distance to # their direct neighbor, i.e., define outliers in absolute amplitude # difference between neighboring extrema. vertical_diff = np.abs(np.diff(rsp_cleaned[extrema])) median_diff = np.median(vertical_diff) min_diff = np.where(vertical_diff > (median_diff * amplitude_min))[0] extrema = extrema[min_diff] # Make sure that the alternation of peaks and troughs is unbroken. If # alternation of sign in extdiffs is broken, remove the extrema that # cause the breaks. amplitudes = rsp_cleaned[extrema] extdiffs = np.sign(np.diff(amplitudes)) extdiffs = np.add(extdiffs[0:-1], extdiffs[1:]) removeext = np.where(extdiffs != 0)[0] + 1 extrema = np.delete(extrema, removeext) amplitudes = np.delete(amplitudes, removeext) return extrema, amplitudes def _rsp_findpeaks_sanitize(extrema, amplitudes): # To be able to consistently calculate breathing amplitude, make sure that # the extrema always start with a trough and end with a peak, since # breathing amplitude will be defined as vertical distance between each # peak and the preceding trough. Note that this also ensures that the # number of peaks and troughs is equal. if amplitudes[0] > amplitudes[1]: extrema = np.delete(extrema, 0) if amplitudes[-1] < amplitudes[-2]: extrema = np.delete(extrema, -1) peaks = extrema[1::2] troughs = extrema[0:-1:2] return peaks, troughs