def test_to_rr(self): ppg = import_ppg().ppg.to_numpy() signal, peaks = oxi_peaks(ppg) rr = to_rr(peaks) assert rr.mean() == 874.2068965517242 rr = to_rr(np.where(peaks)[0]) assert rr.mean() == 874.2068965517242
def plot_oximeter(x, sfreq=75, ax=None): """Plot PPG signal. Parameters ---------- x : 1d array-like or `systole.recording.Oximeter` The ppg signal, or the Oximeter instance used to record the signal. sfreq : int Signal sampling frequency. Default is 75 Hz. ax : `Matplotlib.Axes` or None Where to draw the plot. Default is *None* (create a new figure). Returns ------- ax : `Matplotlib.Axes` The figure. """ if isinstance(x, (list, np.ndarray)): times = np.arange(0, len(x) / sfreq, 1 / sfreq) recording = np.asarray(x) signal, peaks = oxi_peaks(x, new_sfreq=sfreq) threshold = None label = 'Offline estimation' else: times = np.asarray(x.times) recording = np.asarray(x.recording) peaks = np.asarray(x.peaks) threshold = np.asarray(x.threshold) label = 'Online estimation' if ax is None: fig, ax = plt.subplots(figsize=(13, 5)) ax.set_title('Oximeter recording', fontweight='bold') if threshold is not None: ax.plot(times, threshold, linestyle='--', color='gray', label='Threshold') ax.fill_between(x=times, y1=threshold, y2=recording.min(), alpha=0.2, color='gray') ax.plot(times, recording, label='Recording', color='#4c72b0') ax.fill_between(x=times, y1=recording, y2=recording.min(), color='w') ax.plot(times[np.where(peaks)[0]], recording[np.where(peaks)[0]], 'o', color='#c44e52', label=label) ax.set_ylabel('PPG level') ax.set_xlabel('Time (s)') ax.legend() return ax
def test_norm_triggers(self): ppg = import_ppg('1')[0, :] # Import PPG recording signal, peaks = oxi_peaks(ppg) peaks[np.where(peaks)[0] + 1] = 1 peaks[np.where(peaks)[0] + 2] = 1 y = norm_triggers(peaks) assert sum(y) == 378 peaks = -peaks y = norm_triggers(peaks, threshold=-1, direction='lower') assert sum(y) == 378
def test_to_angle(self): """Test to_angles function""" rr = import_rr().rr.values # Create event vector events = rr + np.random.normal(500, 100, len(rr)) ang = to_angles(list(np.cumsum(rr)), list(np.cumsum(events))) assert ~np.any(np.asarray(ang) < 0) assert ~np.any(np.asarray(ang) > np.pi * 2) ppg = import_ppg('1')[0, :] # Import PPG recording signal, peaks = oxi_peaks(ppg) ang = to_angles(peaks, peaks)
def test_heart_rate(self): """Test heart_rate function""" ppg = import_ppg('1')[0, :] # Import PPG recording signal, peaks = oxi_peaks(ppg) heartrate, time = heart_rate(peaks) assert len(heartrate) == len(time) heartrate, time = heart_rate(list(peaks)) assert len(heartrate) == len(time) heartrate, time = heart_rate(peaks, unit='bpm', kind='cubic', sfreq=500) assert len(heartrate) == len(time)
def test_norm_triggers(self): ppg = import_ppg('1')[0, :] # Import PPG recording signal, peaks = oxi_peaks(ppg) peaks[np.where(peaks)[0] + 1] = 1 peaks[np.where(peaks)[0] + 2] = 1 peaks[-1:] = 1 y = norm_triggers(peaks) assert sum(y) == 379 peaks = -peaks.astype(int) y = norm_triggers(peaks, threshold=-1, direction='lower') assert sum(y) == 379 with pytest.raises(ValueError): norm_triggers(None) with pytest.raises(ValueError): norm_triggers(peaks, direction='invalid')
def test_heart_rate(self): """Test heart_rate function""" ppg = import_ppg().ppg.to_numpy() # Import PPG recording signal, peaks = oxi_peaks(ppg) heartrate, time = heart_rate(peaks) assert len(heartrate) == len(time) heartrate, time = heart_rate(list(peaks)) assert len(heartrate) == len(time) heartrate, time = heart_rate(peaks, unit='bpm', kind='cubic', sfreq=500) assert len(heartrate) == len(time) with pytest.raises(ValueError): heartrate, time = heart_rate([1, 2, 3])
def find_peaks(self, **kwargs): """Find peaks in recorded signal. Returns ------- Oximeter instance. The peaks occurences are stored in the `peaks` attribute. Other Parameters ---------------- **kwargs : `~systole.detection.oxi_peaks` properties. """ # Peak detection resampled_signal, peaks = oxi_peaks(self.recording, new_sfreq=75, **kwargs) # R-R intervals (in miliseconds) self.rr = (np.diff(np.where(peaks)[0]) / self.sfreq) * 1000 # Beats per minutes self.bpm = 60000 / self.rr return self
def test_oxi_peaks(self): """Test oxi_peaks function""" ppg = import_ppg('1')[0, :] # Import PPG recording signal, peaks = oxi_peaks(ppg) assert len(signal) == len(peaks) assert np.all(np.unique(peaks) == [0, 1])
def plot_raw(signal, sfreq=75): """Interactive visualization of PPG signal and beats detection. Parameters ---------- signal : `pd.DataFrame` instance or 1d array-like Dataframe of signal recording in the long format. Should contain at least the two following columns: ['time', 'signal']. If an array is provided, will automatically create the DataFrame using th array as signal and *sfreq* as sampling frequency. sfreq : int Signal sampling frequency. Default is 75 Hz. """ if isinstance(signal, pd.DataFrame): # Find peaks - Remove learning phase signal, peaks = oxi_peaks(signal.signal, noise_removal=False) else: signal, peaks = oxi_peaks(signal, noise_removal=False) time = np.arange(0, len(signal)) / 1000 # Extract heart rate hr, time = heart_rate(peaks, sfreq=1000, unit='rr', kind='previous') ############# # Upper panel ############# # Signal ppg_trace = go.Scattergl(x=time, y=signal, mode='lines', name='PPG', hoverinfo='skip', showlegend=False, line=dict(width=1, color='#c44e52')) # Peaks peaks_trace = go.Scattergl(x=time[peaks], y=signal[peaks], mode='markers', name='Peaks', hoverinfo='y', showlegend=False, marker=dict(size=8, color='white', line=dict(width=2, color='DarkSlateGrey'))) ############# # Lower panel ############# # Instantaneous Heart Rate - Lines rr_trace = go.Scattergl(x=time, y=hr, mode='lines', name='R-R intervals', hoverinfo='skip', showlegend=False, line=dict(width=1, color='#4c72b0')) # Instantaneous Heart Rate - Peaks rr_peaks = go.Scattergl(x=time[peaks], y=hr[peaks], mode='markers', name='R-R intervals', showlegend=False, marker=dict(size=6, color='white', line=dict(width=2, color='DarkSlateGrey'))) raw = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=.05, row_titles=['Recording', 'Heart rate']) raw.update_layout(plot_bgcolor="white", paper_bgcolor="white", margin=dict(l=5, r=5, b=5, t=5), autosize=True) raw.add_trace(ppg_trace, 1, 1) raw.add_trace(peaks_trace, 1, 1) raw.add_trace(rr_trace, 2, 1) raw.add_trace(rr_peaks, 2, 1) return raw
def plot_raw(signal, sfreq=75, type='ppg'): """Interactive visualization of PPG signal and beats detection. Parameters ---------- signal : :py:class:`pandas.DataFrame` or 1d array-like Dataframe of signal recording in the long format. Should contain at least one ``'time'`` and one signal colum (can be ``'ppg'`` or ``'ecg'``). If an array is provided, will automatically create the DataFrame using the array as signal and ``sfreq`` as sampling frequency. sfreq : int Signal sampling frequency. Default is 75 Hz. type : str The recording modality. Can be ``'ppg'`` (pulse oximeter) or ``'ecg'`` (electrocardiography). """ import plotly.graph_objs as go from plotly.subplots import make_subplots if isinstance(signal, pd.DataFrame): # Find peaks - Remove learning phase if type == 'ppg': signal, peaks = oxi_peaks(signal.ppg, noise_removal=False) elif type == 'ecg': signal, peaks = ecg_peaks(signal.ecg, method='hamilton', find_local=True) else: if type == 'ppg': signal, peaks = oxi_peaks(signal, noise_removal=False, sfreq=sfreq) elif type == 'ecg': signal, peaks = ecg_peaks(signal, method='hamilton', sfreq=sfreq, find_local=True) time = np.arange(0, len(signal)) / 1000 # Extract heart rate hr, time = heart_rate(peaks, sfreq=1000, unit='rr', kind='linear') ############# # Upper panel ############# # Signal ppg_trace = go.Scattergl(x=time, y=signal, mode='lines', name='PPG', hoverinfo='skip', showlegend=False, line=dict(width=1, color='#c44e52')) # Peaks peaks_trace = go.Scattergl(x=time[peaks], y=signal[peaks], mode='markers', name='Peaks', hoverinfo='y', showlegend=False, marker=dict(size=8, color='white', line=dict(width=2, color='DarkSlateGrey'))) ############# # Lower panel ############# # Instantaneous Heart Rate - Lines rr_trace = go.Scattergl(x=time, y=hr, mode='lines', name='R-R intervals', hoverinfo='skip', showlegend=False, line=dict(width=1, color='#4c72b0')) # Instantaneous Heart Rate - Peaks rr_peaks = go.Scattergl(x=time[peaks], y=hr[peaks], mode='markers', name='R-R intervals', showlegend=False, marker=dict(size=6, color='white', line=dict(width=2, color='DarkSlateGrey'))) raw = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=.05, row_titles=['Recording', 'Heart rate']) raw.update_layout(plot_bgcolor="white", paper_bgcolor="white", margin=dict(l=5, r=5, b=5, t=5), autosize=True, xaxis_title="Time (s)") raw.add_trace(ppg_trace, 1, 1) raw.add_trace(peaks_trace, 1, 1) raw.add_trace(rr_trace, 2, 1) raw.add_trace(rr_peaks, 2, 1) return raw
def report_oxi(signal, file_name='report.png', dpi=600): """Generate HRV report. Parameters ---------- signal : 1d array-like PPG singal. file_name : str Output file name. dpi : int Image quality. """ from systole import import_ppg signal = import_ppg('1')[0] plot_oximeter(signal) # Find peaks signal, peaks = oxi_peaks(signal) # Extract instantaneous heartrate sfreq = 1000 noisy_hr, time = heart_rate(peaks, sfreq=sfreq, unit='bpm', kind='cubic') time = np.arange(0, len(signal) / sfreq, 1 / sfreq) fig = plt.figure(figsize=(8, 13)) gs = fig.add_gridspec(4, 3) fig_ax1 = fig.add_subplot(gs[0, :]) fig_ax1.plot(time, signal, linewidth=.2) fig_ax1.set_ylabel('PPG level') fig_ax1.set_xlim(0, time[-1]) fig_ax1.set_title('Signal', fontweight='bold') fig_ax2 = fig.add_subplot(gs[1, :]) fig_ax2.plot(time, noisy_hr, linewidth=.8, color='r') fig_ax2.set_ylabel('BPM') fig_ax2.set_xlabel('Time (s)') fig_ax2.set_xlim(0, time[-1]) fig_ax2.set_title('RR time-course', fontweight='bold') # HRV rr = np.diff(np.where(peaks)[0]) fig_ax3 = fig.add_subplot(gs[2, 0]) fig_ax3.hist(rr, bins=30, alpha=.5) fig_ax3.hist(rr[(rr <= 400) | (rr >= 1500)], bins=30, color='r') fig_ax3.set_title('Distribution', fontweight='bold') fig_ax3.set_ylabel('Count') fig_ax3.set_xlabel('RR(s)') fig_ax4 = fig.add_subplot(gs[2, 1]) plot_psd(rr, show=True, ax=fig_ax4) fig_ax5 = fig.add_subplot(gs[2, 2]) fig_ax5.plot(rr[:-1], rr[1:], color='gray', markersize=1, alpha=0.8) fig_ax5.set_title('Pointcare Plot', fontweight='bold') fig_ax5.set_ylabel('$RR_{n+1}$') fig_ax5.set_xlabel('$RR_n$') plt.tight_layout(h_pad=0.05) if file_name is not None: plt.savefig(file_name, dpi=dpi) plt.close()
def test_oxi_peaks(self): """Test oxi_peaks function""" df = import_ppg() # Import PPG recording signal, peaks = oxi_peaks(df.ppg.to_numpy()) assert len(signal) == len(peaks) assert np.all(np.unique(peaks) == [0, 1])