From 07ad050e5f6d11a26cae46ed6bf7b49882b6096a Mon Sep 17 00:00:00 2001 From: Nicolas Legrand Date: Wed, 2 Sep 2020 18:10:49 +0200 Subject: [PATCH] - Docstrings, README and examples --- README.rst | 93 +++++------------------------ examples/Tutorial_HRV.py | 54 +++++++++++++++++ examples/Tutorial_recording.py | 87 ++++++++++++++++++++++++++++ examples/plot_ECGProcessing.py | 7 ++- source/api.rst | 3 + systole/detection.py | 15 +++-- systole/hrv.py | 103 +++++++++++++++++++++++---------- systole/plotly.py | 50 +++++++++++----- systole/recording.py | 9 +-- systole/tests/test_plotting.py | 1 - 10 files changed, 284 insertions(+), 138 deletions(-) create mode 100644 examples/Tutorial_HRV.py create mode 100644 examples/Tutorial_recording.py diff --git a/README.rst b/README.rst index 8dbe2fec..4584b4eb 100644 --- a/README.rst +++ b/README.rst @@ -43,92 +43,25 @@ The following packages are required to use Systole: * Pandas (>=0.24) * Matplotlib (>=3.0.2) * Seaborn (>=0.9.0) +* py-ecg-detectors (>=1.0.2) -Recording -========= - -Systole natively supports the recording of PPG signals through the `Nonin 3012LP Xpod USB pulse oximeter `_ together with the `Nonin 8000SM 'soft-clip' fingertip sensors `_. -It can easily interface with `PsychoPy `_ to record PPG signal during psychological experiments, and to synchronize stimulus deliver to e.g., systole or diastole. - -For example, you can record and plot data in less than 6 lines of code: - -.. code-block:: python - - import serial - from systole.recording import Oximeter - ser = serial.Serial('COM4') # Add your USB port here - - # Open serial port, initialize and plot recording for Oximeter - oxi = Oximeter(serial=ser).setup().read(duration=10) - - -Interfacing with PsychoPy -------------------------- - -The ``Oximeter`` class can be used together with a stimulus presentation software to record cardiac activity during psychological experiments. - -* The ``read()`` method - -will record for a predefined amount of time (specified by the ``duration`` parameter, in seconds). This 'serial mode' is the easiest and most robust method, but it does not allow the execution of other instructions in the meantime. - -.. code-block:: python - - # Code 1 {} - oximeter.read(duration=10) - # Code 2 {} - -* The ``readInWaiting()`` method +Interactive plotting functions and reports generation will also require the following packages to be installed: -will only read the bytes temporally stored in the USB buffer. For the Nonin device, this represents up to 10 seconds of recording (this procedure should be executed at least one time every 10 seconds for a continuous recording). When inserted into a while loop, it can record PPG signal in parallel with other commands. +* plotly (>=4.8.0) +* plotly_express (>=0.4.1) -.. code-block:: python - - import time - tstart = time.time() - while time.time() - tstart < 10: - oximeter.readInWaiting() - # Insert code here {...} - -Online detection ----------------- - -Online heart beat detection, for cardiac-stimulus synchrony: - -.. code-block:: python - - import serial - import time - from systole.recording import Oximeter - - # Open serial port - ser = serial.Serial('COM4') # Change this value according to your setup - - # Create an Oxymeter instance and initialize recording - oxi = Oximeter(serial=ser, sfreq=75, add_channels=4).setup() +For an overview of all the recording functionalities, you can refer to the following tutorials: - # Online peak detection for 10 seconds - tstart = time.time() - while time.time() - tstart < 10: - while oxi.serial.inWaiting() >= 5: - paquet = list(oxi.serial.read(5)) - oxi.add_paquet(paquet[2]) # Add new data point - if oxi.peaks[-1] == 1: - print('Heartbeat detected') +* Recording +* Artefacts detection and artefacts correction +* Heart rate variability -Peaks detection -=============== - -Heartbeats can be detected in the PPG signal either online or offline. - -Methods from clipping correction and peak detection algorithm is adapted from [#]_. - -.. code-block:: python - - # Plot data - oxi.plot_oximeter() +Recording +========= -.. figure:: https://github.com/embodied-computation-group/systole/raw/master/Images/recording.png - :align: center +Systole natively supports recording of physiological signals from the following setups: +* `Nonin 3012LP Xpod USB pulse oximeter `_ together with the `Nonin 8000SM 'soft-clip' fingertip sensors `_ (USB). +* Remote Data Access (RDA) via BrainVision Recorder together with Brain product ExG amplifier ``_ (Ethernet). Artefact correction =================== diff --git a/examples/Tutorial_HRV.py b/examples/Tutorial_HRV.py new file mode 100644 index 00000000..12281589 --- /dev/null +++ b/examples/Tutorial_HRV.py @@ -0,0 +1,54 @@ +""" +Recording +========= + + +""" + +# Author: Nicolas Legrand +# Licence: GPL v3 + +# It can easily interface with `PsychoPy `_ to +# record PPG signal during psychological experiments, and to synchronize +# stimulus deliver to e.g., systole or diastole. + +# For example, you can record and plot data in less than 6 lines of code: + + +#%% +# Event related cardiac deceleration +# ---------------------------------- +import serial +from systole.recording import Oximeter +ser = serial.Serial('COM4') # Add your USB port here + +# Open serial port, initialize and plot recording for Oximeter +oxi = Oximeter(serial=ser).setup().read(duration=10) + + +Interfacing with PsychoPy +------------------------- + +The ``Oximeter`` class can be used together with a stimulus presentation software to record cardiac activity during psychological experiments. + +* The ``read()`` method + +will record for a predefined amount of time (specified by the ``duration`` parameter, in seconds). This 'serial mode' is the easiest and most robust method, but it does not allow the execution of other instructions in the meantime. + +.. code-block:: python + + # Code 1 {} + oximeter.read(duration=10) + # Code 2 {} + +* The ``readInWaiting()`` method + +will only read the bytes temporally stored in the USB buffer. For the Nonin device, this represents up to 10 seconds of recording (this procedure should be executed at least one time every 10 seconds for a continuous recording). When inserted into a while loop, it can record PPG signal in parallel with other commands. + +.. code-block:: python + + import time + tstart = time.time() + while time.time() - tstart < 10: + oximeter.readInWaiting() + # Insert code here {...} diff --git a/examples/Tutorial_recording.py b/examples/Tutorial_recording.py new file mode 100644 index 00000000..8fff3323 --- /dev/null +++ b/examples/Tutorial_recording.py @@ -0,0 +1,87 @@ +""" +Recording PPG signal +==================== +""" + +# Author: Nicolas Legrand +# Licence: GPL v3 + +# The py:class:systole.recording.Oximeter class can be used to read incoming +# PPG signal from `Nonin 3012LP Xpod USB pulse oximeter +# `_ together with the `Nonin 8000SM +# 'soft-clip' fingertip sensors `_. +# This function can easily be integrated with other stimulus presentation +# software lie `PsychoPy `_ to record cardiac +# activity during psychological experiments, or to synchronize stimulus +# delivery with cardiac phases (e.g. systole or diastole). + + +#%% +# Reading +# ------- +# Recording and plotting your first time-series will only require 5 lines +# of code: + +import serial +from systole.recording import Oximeter +ser = serial.Serial('COM4') # Add your USB port here + +# Open serial port, initialize and plot recording for Oximeter +oxi = Oximeter(serial=ser).setup().read(duration=10) + +# The signal can be directly plotted using built-in functions. +oxi.plot_oximeter() + +############################################################################## +# .. figure:: https://github.com/embodied-computation-group/systole/raw/master/Images/recording.png +# :align: center +############################################################################## + +#%% +# Interfacing with PsychoPy +# ------------------------- + +# * The ``read()`` method will record for a predefined amount of time +# (specified by the ``duration`` parameter, in seconds). This 'serial mode' +# is the easiest and most robust method, but it does not allow the execution +# of other instructions in the meantime. + +# Code 1 {} +oximeter.read(duration=10) +# Code 2 {} + +# * The ``readInWaiting()`` method will only read the bytes temporally stored +# in the USB buffer. For the Nonin device, this represents up to 10 seconds of +# recording (this procedure should be executed at least one time every 10 +# seconds for a continuous recording). When inserted into a while loop, it can +# record PPG signal in parallel with other commands. + +import time +tstart = time.time() +while time.time() - tstart < 10: + oximeter.readInWaiting() + # Insert code here {...} + +#%% +# Online detection +# ---------------- +# Online heart beat detection, for cardiac-stimulus synchrony + +import serial +import time +from systole.recording import Oximeter + +# Open serial port +ser = serial.Serial('COM4') # Change this value according to your setup + +# Create an Oxymeter instance and initialize recording +oxi = Oximeter(serial=ser, sfreq=75, add_channels=4).setup() + +# Online peak detection for 10 seconds +tstart = time.time() +while time.time() - tstart < 10: + while oxi.serial.inWaiting() >= 5: + paquet = list(oxi.serial.read(5)) + oxi.add_paquet(paquet[2]) # Add new data point + if oxi.peaks[-1] == 1: + print('Heartbeat detected') diff --git a/examples/plot_ECGProcessing.py b/examples/plot_ECGProcessing.py index 8cbaa4ef..2e1a9bbd 100644 --- a/examples/plot_ECGProcessing.py +++ b/examples/plot_ECGProcessing.py @@ -27,7 +27,7 @@ # --------------- # The peaks detection algorithms are imported from the py-ecg-detectors module: # https://github.com/berndporr/py-ecg-detectors -signal, peaks = ecg_peaks(signal_df.ecg, method='hamilton', sfreq=2000, +signal, peaks = ecg_peaks(signal_df.ecg, method='hamilton', sfreq=1000, find_local=True) #%% @@ -50,10 +50,12 @@ neutral[ np.round(np.where(signal_df.stim.to_numpy() == 3)[0]).astype(int)] = 1 +#%% # Event related plot +# ------------------ sns.set_context('talk') fig, ax = plt.subplots(figsize=(8, 5)) -for cond, col, data in zip( +for cond, data, col in zip( ['Neutral', 'Disgust'], [neutral, disgust], [sns.xkcd_rgb["denim blue"], sns.xkcd_rgb["pale red"]]): @@ -72,6 +74,7 @@ ax.set_ylabel('Heart Rate (BPM)') ax.set_title('Instantaneous heart rate after neutral and disgusting images') sns.despine() +plt.tight_layout() #%% diff --git a/source/api.rst b/source/api.rst index f33e666d..1b4a9b58 100644 --- a/source/api.rst +++ b/source/api.rst @@ -17,6 +17,7 @@ Detection :toctree: generated/ oxi_peaks + ecg_peaks rr_artefacts interpolate_clipping @@ -72,6 +73,7 @@ Recording :toctree: generated/ recording.Oximeter + recording.BrainVisionExG Utils ----- @@ -86,3 +88,4 @@ Utils heart_rate to_angles to_epochs + to_rr diff --git a/systole/detection.py b/systole/detection.py index 4f268339..9d7e1dd3 100644 --- a/systole/detection.py +++ b/systole/detection.py @@ -49,17 +49,16 @@ def oxi_peaks(x, sfreq=75, win=1, new_sfreq=1000, clipping=True, >>> df = import_ppg() # Import PPG recording >>> signal, peaks = oxi_peaks(df.ppg.to_numpy()) >>> print(f'{sum(peaks)} peaks detected.') + 378 peaks detected. References ---------- - Some of the processing steps were adapted from the HeartPy toolbox: - https://python-heart-rate-analysis-toolkit.readthedocs.io/en/latest/index.html - .. [1] van Gent, P., Farah, H., van Nes, N. and van Arem, B., 2019. Analysing Noisy Driver Physiology Real-Time Using Off-the-Shelf Sensors: Heart Rate Analysis Software from the Taking the Fast Lane Project. Journal of Open Research Software, 7(1), p.32. DOI: http://doi.org/10.5334/jors.241 """ + if isinstance(x, list): x = np.asarray(x) @@ -151,12 +150,14 @@ def ecg_peaks(x, sfreq=1000, new_sfreq=1000, method='pan-tompkins', >>> signal, peaks = ecg_peaks(signal_df.ecg.to_numpy(), method='hamilton', >>> sfreq=2000, find_local=True) >>> print(f'{sum(peaks)} peaks detected.') + 24 peaks detected. References ---------- .. [#] Howell, L., Porr, B. Popular ECG R peak detectors written in python. DOI: 10.5281/zenodo.3353396 """ + if isinstance(x, list): x = np.asarray(x) @@ -256,6 +257,8 @@ def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2): >>> rr = simulate_rr() # Simulate RR time series >>> artefacts = rr_artefacts(rr) >>> print(artefacts.keys()) + dict_keys(['subspace1', 'subspace2', 'subspace3', 'mRR', 'ectopic', 'long', + 'short', 'missed', 'extra', 'threshold1', 'threshold2']) References ---------- @@ -388,14 +391,14 @@ def interpolate_clipping(signal, threshold=255): Examples -------- .. plot:: + >>> import matplotlib.pyplot as plt >>> from systole import import_ppg >>> from systole.detection import interpolate_clipping >>> df = import_ppg() - >>> clean_signal = interpolate_clipping(df.ppg.to_numpy(), - >>> threshold=255) + >>> clean_signal = interpolate_clipping(df.ppg.to_numpy()) >>> plt.plot(df.time, clean_signal, color='#F15854') - >>> plt.plot(df.time, ppg, color='#5DA5DA') + >>> plt.plot(df.time, df.ppg, color='#5DA5DA') >>> plt.axhline(y=255, linestyle='--', color='k') >>> plt.xlabel('Time (s)') >>> plt.ylabel('PPG level (a.u)') diff --git a/systole/hrv.py b/systole/hrv.py index b52e1209..ef45ad1c 100644 --- a/systole/hrv.py +++ b/systole/hrv.py @@ -12,7 +12,7 @@ def nnX(x, t=50): Parameters ---------- x : array like - Length of R-R intervals (in miliseconds). + Interval time-series (R-R, beat-to-beat...), in miliseconds. t : int Threshold value: Defaut is set to 50 ms to calculate the nn50 index. @@ -37,7 +37,7 @@ def pnnX(x, t=50): Parameters ---------- x : array like - Length of R-R intervals (in miliseconds). + Interval time-series (R-R, beat-to-beat...), in miliseconds. t : int Threshold value: Defaut is set to 50 ms to calculate the nn50 index. @@ -66,7 +66,7 @@ def rmssd(x): Parameters ---------- x : array like - Length of R-R intervals (in miliseconds). + Interval time-series (R-R, beat-to-beat...), in miliseconds. Returns ------- @@ -99,26 +99,38 @@ def time_domain(x): Parameters ---------- - x : array-like - Length of R-R intervals (in miliseconds). + x : 1d array-like + Interval time-series (R-R, beat-to-beat...), in miliseconds. Returns ------- stats : :py:class:`pandas.DataFrame` - Time domain summary statistics: - - 'Mean RR' : Mean of R-R intervals. - 'Mean BPM' : Mean of beats per minutes. - 'Median RR : Median of R-R intervals'. - 'Median BPM' : Meidan of beats per minutes. - 'MinRR' : Minimum R-R intervals. - 'MinBPM' : Minimum beats per minutes. - 'MaxRR' : Maximum R-R intervals. - 'MaxBPM' : Maximum beats per minutes. - 'SDNN' : Standard deviation of successive differences. - 'RMSSD' : Root Mean Square of the Successive Differences. - 'NN50' : number of successive differences larger than 50ms. - 'pNN50' : Proportion of successive difference larger than 50ms. + Time domain summary statistics. + * ``'Mean RR'`` : Mean of R-R intervals. + * ``'Mean BPM'`` : Mean of beats per minutes. + * ``'Median RR'`` : Median of R-R intervals'. + * ``'Median BPM'`` : Meidan of beats per minutes. + * ``'MinRR'`` : Minimum R-R intervals. + * ``'MinBPM'`` : Minimum beats per minutes. + * ``'MaxRR'`` : Maximum R-R intervals. + * ``'MaxBPM'`` : Maximum beats per minutes. + * ``'SDNN'`` : Standard deviation of successive differences. + * ``'RMSSD'`` : Root Mean Square of the Successive Differences. + * ``'NN50'`` : number of successive differences larger than 50ms. + * ``'pNN50'`` : Proportion of successive difference larger than 50ms. + + See also + -------- + frequency_domain, nonlinear + + Notes + ----- + The dataframe containing the summary statistics is returned in the long + format to facilitate the creation of group summary data frame that can + easily be transferred to other plotting or statistics library. You can + easily convert it into a wide format for a subject-level inline report + using the py:pandas.pivot_table() function: + >>> pd.pivot_table(stats, values='Values', columns='Metric') """ if isinstance(x, list): x = np.asarray(x) @@ -178,22 +190,40 @@ def frequency_domain(x, sfreq=5, method='welch', fbands=None): Parameters ---------- x : list or 1d array-like - Length of R-R intervals (in miliseconds). + Interval time-series (R-R, beat-to-beat...), in miliseconds. sfreq : int - The sampling frequency. + The sampling frequency (Hz). method : str - The method used to extract freauency power. Default set to `'welch'`. + The method used to extract freauency power. Default is ``'welch'``. fbands : None | dict, optional Dictionary containing the names of the frequency bands of interest (str), their range (tuples) and their color in the PSD plot. Default is - {'vlf': ['Very low frequency', (0.003, 0.04), 'b'], - 'lf': ['Low frequency', (0.04, 0.15), 'g'], - 'hf': ['High frequency', (0.15, 0.4), 'r']} + >>> {'vlf': ['Very low frequency', (0.003, 0.04), 'b'], + >>> 'lf': ['Low frequency', (0.04, 0.15), 'g'], + >>> 'hf': ['High frequency', (0.15, 0.4), 'r']} Returns ------- stats : :py:class:`pandas.DataFrame` - DataFrame of HRV parameters (frequency domain) + Frequency domain summary statistics. + * ``'power_vlf_per'`` : Very low frequency power (%). + * ``'power_lf_per'`` : Low frequency power (%). + * ``'power_hf_per'`` : High frequency power (%). + * ``'power_lf_nu'`` : Low frequency power (normalized units). + * ``'power_hf_nu'`` : High frequency power (normalized units). + + See also + -------- + time_domain, nonlinear + + Notes + ----- + The dataframe containing the summary statistics is returned in the long + format to facilitate the creation of group summary data frame that can + easily be transferred to other plotting or statistics library. You can + easily convert it into a wide format for a subject-level inline report + using the py:pandas.pivot_table() function: + >>> pd.pivot_table(stats, values='Values', columns='Metric') """ # Interpolate R-R interval time = np.cumsum(x) @@ -262,17 +292,32 @@ def frequency_domain(x, sfreq=5, method='welch', fbands=None): def nonlinear(x): - """Extract the frequency domain features of heart rate variability. + """Extract the non-linear features of heart rate variability. Parameters ---------- x : list or numpy array - Length of R-R intervals (in miliseconds). + Interval time-series (R-R, beat-to-beat...), in miliseconds. Returns ------- stats : :py:class:`pandas.DataFrame` - DataFrame of HRV parameters (frequency domain) + Non-linear domain summary statistics. + * ``'SD1'`` : SD1. + * ``'SD2'`` : SD2. + + See also + -------- + time_domain, frequency_domain + + Notes + ----- + The dataframe containing the summary statistics is returned in the long + format to facilitate the creation of group summary data frame that can + easily be transferred to other plotting or statistics library. You can + easily convert it into a wide format for a subject-level inline report + using the py:pandas.pivot_table() function: + >>> pd.pivot_table(stats, values='Values', columns='Metric') """ diff_rr = np.diff(x) diff --git a/systole/plotly.py b/systole/plotly.py index 3376f9ae..d99565f9 100644 --- a/systole/plotly.py +++ b/systole/plotly.py @@ -14,15 +14,16 @@ def plot_raw(signal, sfreq=75, type='ppg'): Parameters ---------- - signal : `pd.DataFrame` instance or 1d array-like + signal : :py:class:`pandas.DataFrame` or 1d array-like Dataframe of signal recording in the long format. Should contain at - one 'time' and one signal colum (can be 'ppg' or 'ecg'). If an array is - provided, will automatically create the DataFrame using th array as - signal and *sfreq* as sampling frequency. + 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' + The recording modality. Can be ``'ppg'`` (pulse oximeter) or ``'ecg'`` (electrocardiography). """ import plotly.graph_objs as go @@ -97,9 +98,10 @@ def plot_ectopic(rr=None, artefacts=None): Parameters ---------- rr : 1d array-like or None - The RR time serie. + Interval time-series (R-R, beat-to-beat...), in miliseconds. artefacts : dict or None - The artefacts detected using *systole.detection.rr_artefacts()*. + The artefacts detected using + :py:func:`systole.detection.rr_artefacts()`. Returns ------- @@ -227,9 +229,10 @@ def plot_shortLong(rr=None, artefacts=None): Parameters ---------- rr : 1d array-like or None - The RR time serie. + Interval time-series (R-R, beat-to-beat...), in miliseconds. artefacts : dict or None - The artefacts detected using *systole.detection.rr_artefacts()*. + The artefacts detected using + :py:func:`systole.detection.rr_artefacts()`. Returns ------- @@ -238,7 +241,7 @@ def plot_shortLong(rr=None, artefacts=None): Notes ----- - If both *rr* or *artefacts* are provided, will recompute *artefacts* + If both ``rr`` or ``artefacts`` are provided, will recompute ``artefacts`` given the current rr time-series. """ import plotly_express as px @@ -350,12 +353,12 @@ def plot_shortLong(rr=None, artefacts=None): def plot_subspaces(rr): - """Plot hrv subspace as described by Lipponen & Tarvainen (2019). + """Plot hrv subspace as described by Lipponen & Tarvainen (2019) [#]_. Parameters ---------- rr : 1d array-like - The dataframe containing the recording. + Interval time-series (R-R, beat-to-beat...), in miliseconds. Returns ------- @@ -364,7 +367,7 @@ def plot_subspaces(rr): References ---------- - [1] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for + ..[#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for heart rate variability time series artefact correction using novel beat classification. Journal of Medical Engineering & Technology, 43(3), 173–181. https://doi.org/10.1080/03091902.2019.1640306 @@ -407,7 +410,12 @@ def plot_frequency(rr): Parameters ---------- rr : 1d array-like - Time series of R-R intervals. + Interval time-series (R-R, beat-to-beat...), in miliseconds. + + Returns + ------- + fig : `go.Figure` + The figure. """ import plotly.graph_objs as go from plotly.subplots import make_subplots @@ -482,7 +490,12 @@ def plot_nonlinear(rr): Parameters ---------- rr : 1d array-like - Time sere of R-R intervals. + Interval time-series (R-R, beat-to-beat...), in miliseconds. + + Returns + ------- + fig : `go.Figure` + The figure. """ import plotly.graph_objs as go from plotly.subplots import make_subplots @@ -540,7 +553,12 @@ def plot_timedomain(rr): Parameters ---------- rr : 1d array-like - Time sere of R-R intervals. + Interval time-series (R-R, beat-to-beat...), in miliseconds. + + Returns + ------- + fig : `go.Figure` + The figure. """ import plotly.graph_objs as go from plotly.subplots import make_subplots diff --git a/systole/recording.py b/systole/recording.py index 8b787052..d4138c10 100644 --- a/systole/recording.py +++ b/systole/recording.py @@ -421,10 +421,10 @@ class BrainVisionExG(): `exg` dictionnary. .. warning:: The signals received fom the host are appened to a list. This - process can require more time at each iteration as the signal length - increase in memory. You should alway make sure that this will not interfer - with other task and regularly save intermediate recording to save - resources. + process can require more time at each iteration as the signal length + increase in memory. You should alway make sure that this will not + interfer with other task and regularly save intermediate recording to + save resources. Notes ----- @@ -432,6 +432,7 @@ class BrainVisionExG(): Brain Products on the following link: https://www.brainproducts.com/downloads.php?kid=2 """ + def __init__(self, ip, sfreq, port=51244): self.ip = ip diff --git a/systole/tests/test_plotting.py b/systole/tests/test_plotting.py index 42b4bb7c..f9bd399f 100644 --- a/systole/tests/test_plotting.py +++ b/systole/tests/test_plotting.py @@ -6,7 +6,6 @@ import pytest import matplotlib from unittest import TestCase - from systole.plotting import plot_hr, plot_events, plot_oximeter,\ plot_subspaces, circular, plot_circular, plot_psd from systole import import_ppg, import_rr, serialSim