diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..0435a0d2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203, E722, E501 +exclude = + __init__.py \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3acea11b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*-checkpoint.ipynb +mypyreports/ \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..88c43cc2 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,8 @@ +[settings] +known_third_party = ecgdetectors,matplotlib,numpy,pandas,plotly,psychopy,pytest,requests,scipy,seaborn,serial,sphinx_bootstrap_theme,tqdm +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +ensure_newline_before_comments = True +line_length = 88 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..ae1f42e7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort + files: ^systole/ +- repo: https://github.com/ambv/black + rev: stable + hooks: + - id: black + language_version: python3 + files: ^systole/ +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: flake8 + files: ^systole/ +- repo: https://github.com/pre-commit/mirrors-mypy + rev: '' # Use the sha / tag you want to point at + hooks: + - id: mypy + files: ^systole/ + args: [--ignore-missing-imports] \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 71e385f9..c5ffb7e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,14 +4,19 @@ python: - "3.6" - "3.7" - "3.8" + - "3.9" install: - pip install -r requirements-test.txt - pip install -r requirements.txt - - pip install coverage pytest + - pip install coverage pytest black mypy flake8 isort script: - coverage run -m pytest + - black --check ./systole/ + - isort ./systole/ + - mypy ./systole/ --ignore-missing-imports --follow-imports=skip + - flake8 ./systole/ after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/Images/recording.png b/Images/recording.png deleted file mode 100644 index 6cfb74c6..00000000 Binary files a/Images/recording.png and /dev/null differ diff --git a/MANIFEST.in b/MANIFEST.in index dbec1b8d..fcffc68a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,11 +2,3 @@ include README.rst include LICENSE include requirements.txt - -# Add datasets files -include systole/datasets/ppg.npy -include systole/datasets/rr.txt -include systole/datasets/Task1_ECG.npy -include systole/datasets/Task1_EDA.npy -include systole/datasets/Task1_Respiration.npy -include systole/datasets/Task1_Stim.npy diff --git a/README.rst b/README.rst index 4584b4eb..e84b85bd 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,18 @@ .. image:: https://codecov.io/gh/embodied-computation-group/systole/branch/master/graph/badge.svg :target: https://codecov.io/gh/embodied-computation-group/systole +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + +.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 + :target: https://pycqa.github.io/isort/ + +.. image:: http://www.mypy-lang.org/static/mypy_badge.svg + :target: http://mypy-lang.org/ + +.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white + :target: https://github.com/pre-commit/pre-commit + ================ .. figure:: https://github.com/embodied-computation-group/systole/raw/master/source/images/banner.png @@ -38,35 +50,44 @@ Systole can be installed using pip: The following packages are required to use Systole: -* Numpy (>=1.15) -* SciPy (>=1.3.0) -* Pandas (>=0.24) -* Matplotlib (>=3.0.2) -* Seaborn (>=0.9.0) -* py-ecg-detectors (>=1.0.2) +* `Numpy `_ (>=1.15) +* `SciPy `_ (>=1.3.0) +* `Pandas `_ (>=0.24) +* `Matplotlib `_ (>=3.0.2) +* `Seaborn `_ (>=0.9.0) +* `py-ecg-detectors `_ (>=1.0.2) Interactive plotting functions and reports generation will also require the following packages to be installed: -* plotly (>=4.8.0) -* plotly_express (>=0.4.1) +* `Plotly `_ (>=4.8.0) -For an overview of all the recording functionalities, you can refer to the following tutorials: +Tutorial +======== + +For an overview of all the recording functionalities, you can refer to the following examples: * Recording * Artefacts detection and artefacts correction * Heart rate variability +For an introduction to Systole and cardiac signal analysis, you can refer to the following tutorial: + +* Introduction to cardiac signal analysis - |Colab badge| - `Jupyter Book `_ + +.. |Colab badge| image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/github/LegrandNico/Notebooks/blob/main/IntroductionCardiacSignalAnalysis.ipynb + Recording ========= 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). +* Remote Data Access (RDA) via BrainVision Recorder together with `Brain product ExG amplifier `_ (Ethernet). Artefact correction =================== -Systole implements the artefact rejection method recently proposed by Lipponen & Tarvainen (2019) [#]_. +Systole implements systolic peak detection inspired by van Gent et al. (2019) [#]_ and the artefact rejection method recently proposed by Lipponen & Tarvainen (2019) [#]_. .. code-block:: python @@ -137,7 +158,7 @@ References **Peak detection (PPG signal)** -.. [#] van Gent, P., Farah, H., van Nes, N., & van Arem, B. (2019). HeartPy: A novel heart rate algorithm for the analysis of noisy signals. Transportation Research Part F: Traffic Psychology and Behaviour, 66, 368–378. https://doi.org/10.1016/j.trf.2019.09.015 +.. [#] van Gent, P., Farah, H., van Nes, N., & van Arem, B. (2019). HeartPy: A novel heart rate algorithm for the analysis of noisy signals. *Transportation Research Part F: Traffic Psychology and Behaviour, 66, 368–378*. https://doi.org/10.1016/j.trf.2019.09.015 **Artefact detection and correction:** diff --git a/examples/Tutorial_HRV.py b/examples/Tutorial_HRV.py deleted file mode 100644 index 12281589..00000000 --- a/examples/Tutorial_HRV.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -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 index 8fff3323..abf0eb52 100644 --- a/examples/Tutorial_recording.py +++ b/examples/Tutorial_recording.py @@ -22,9 +22,13 @@ # Recording and plotting your first time-series will only require 5 lines # of code: +import time + import serial + from systole.recording import Oximeter -ser = serial.Serial('COM4') # Add your USB port here + +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) @@ -56,7 +60,7 @@ # 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() @@ -67,12 +71,9 @@ # ---------------- # 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 +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() @@ -84,4 +85,4 @@ paquet = list(oxi.serial.read(5)) oxi.add_paquet(paquet[2]) # Add new data point if oxi.peaks[-1] == 1: - print('Heartbeat detected') + print("Heartbeat detected") diff --git a/examples/plot_ArtefactsCorrection.py b/examples/plot_ArtefactsCorrection.py index 23081216..86fe1b80 100644 --- a/examples/plot_ArtefactsCorrection.py +++ b/examples/plot_ArtefactsCorrection.py @@ -20,13 +20,13 @@ # ectopic beats. This method is more relevant for HRV analyse of long recording # where the timing of experimental events is not important. +import matplotlib.pyplot as plt #%% import numpy as np -import matplotlib.pyplot as plt + from systole import simulate_rr -from systole.plotting import plot_subspaces from systole.correction import correct_peaks, correct_rr - +from systole.plotting import plot_subspaces #%% Method 1 - Peaks correction # ############################# diff --git a/examples/plot_ArtefactsDetection.py b/examples/plot_ArtefactsDetection.py index 71f6e97b..0f955bcd 100644 --- a/examples/plot_ArtefactsDetection.py +++ b/examples/plot_ArtefactsDetection.py @@ -12,8 +12,8 @@ #%% from systole.detection import rr_artefacts -from systole.utils import simulate_rr from systole.plotting import plot_subspaces +from systole.utils import simulate_rr #%% # RR artefacts diff --git a/examples/plot_ECGProcessing.py b/examples/plot_ECGProcessing.py deleted file mode 100644 index 2e1a9bbd..00000000 --- a/examples/plot_ECGProcessing.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -ECG preprocessing and R wave detection -====================================== - -This notebook describe ECG signal processing, from R wave detection to heart -rate variability and evoked heart rate activity. -""" - -# Author: Nicolas Legrand -# Licence: GPL v3 - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -import seaborn as sns -from systole import import_dataset -from systole.detection import ecg_peaks -from systole.utils import heart_rate, to_epochs - -#%% -# Loading ECG dataset -# ------------------- -signal_df = import_dataset() - -#%% -# Finding R peaks -# --------------- -# 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=1000, - find_local=True) - -#%% -# Heart Rate Variability -# ---------------------- - -#%% -# Event related cardiac deceleration -# ---------------------------------- - -# Extract instantaneous heart rate -heartrate, new_time = heart_rate(peaks, kind='cubic', unit='bpm') - -# Downsample the stim events channel -# to fit with the new sampling frequency (1000 Hz) -neutral, disgust = np.zeros(len(new_time)), np.zeros(len(new_time)) - -disgust[ - np.round(np.where(signal_df.stim.to_numpy() == 2)[0]).astype(int)] = 1 -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, data, col in zip( - ['Neutral', 'Disgust'], [neutral, disgust], - [sns.xkcd_rgb["denim blue"], sns.xkcd_rgb["pale red"]]): - - # Epoch intantaneous heart rate - # and downsample to 2 Hz to save memory - epochs = to_epochs(heartrate, data, tmin=0, tmax=11)[:, ::500] - - # Plot - df = pd.DataFrame(epochs).melt() - df.variable /= 2 - sns.lineplot(data=df, x='variable', y='value', ci=68, label=cond, - color=col, ax=ax) - -ax.set_xlim(0, 10) -ax.set_xlabel('Time (s)') -ax.set_ylabel('Heart Rate (BPM)') -ax.set_title('Instantaneous heart rate after neutral and disgusting images') -sns.despine() -plt.tight_layout() - - -#%% -# References -# ---------- -# .. [#] Porr, B., & Howell, L. (2019). R-peak detector stress test with a new -# noisy ECG database reveals significant performance differences amongst -# popular detectors. Cold Spring Harbor Laboratory. -# https://doi.org/10.1101/722397 diff --git a/examples/plot_HeartBeatEvokedArpeggios.py b/examples/plot_HeartBeatEvokedArpeggios.py index 7121a50e..af7981e1 100644 --- a/examples/plot_HeartBeatEvokedArpeggios.py +++ b/examples/plot_HeartBeatEvokedArpeggios.py @@ -18,18 +18,18 @@ # Author: Nicolas Legrand # Licence: GPL v3 +import itertools import time + +import matplotlib.pyplot as plt import numpy as np -import itertools import seaborn as sns -import matplotlib.pyplot as plt from psychopy.sound import Sound -from systole.utils import norm_triggers from systole import serialSim -from systole.utils import to_angles from systole.plotting import circular from systole.recording import Oximeter +from systole.utils import norm_triggers, to_angles #%% # Recording @@ -58,10 +58,10 @@ #%% # Create an Oxymeter instance, initialize recording and record for 10 seconds -beat = Sound('C', secs=0.1) -diastole1 = Sound('E', secs=0.1) -diastole2 = Sound('G', secs=0.1) -diastole3 = Sound('Bfl', secs=0.1) +beat = Sound("C", secs=0.1) +diastole1 = Sound("E", secs=0.1) +diastole2 = Sound("G", secs=0.1) +diastole3 = Sound("Bfl", secs=0.1) systoleTime1, systoleTime2, systoleTime3 = None, None, None tstart = time.time() @@ -78,7 +78,7 @@ # T + 0 if oxi.peaks[-1] == 1: - beat = Sound('C', secs=0.1) + beat = Sound("C", secs=0.1) beat.play() systoleTime1 = time.time() systoleTime2 = time.time() @@ -86,32 +86,30 @@ # T + 1/4 if systoleTime1 is not None: - if time.time() - systoleTime1 >= ((oxi.instant_rr[-1]/4)/1000): - diastole1 = Sound('E', secs=0.1) + if time.time() - systoleTime1 >= ((oxi.instant_rr[-1] / 4) / 1000): + diastole1 = Sound("E", secs=0.1) diastole1.play() systoleTime1 = None # T + 2/4 if systoleTime2 is not None: - if time.time() - systoleTime2 >= ( - ((oxi.instant_rr[-1]/4) * 2)/1000): - diastole2 = Sound('G', secs=0.1) + if time.time() - systoleTime2 >= (((oxi.instant_rr[-1] / 4) * 2) / 1000): + diastole2 = Sound("G", secs=0.1) diastole2.play() systoleTime2 = None # T + 3/4 if systoleTime3 is not None: - if time.time() - systoleTime3 >= ( - ((oxi.instant_rr[-1]/4) * 3)/1000): - diastole3 = Sound('A', secs=0.1) + if time.time() - systoleTime3 >= (((oxi.instant_rr[-1] / 4) * 3) / 1000): + diastole3 = Sound("A", secs=0.1) diastole3.play() systoleTime3 = None # Track the note status - oxi.channels['Channel_0'][-1] = beat.status - oxi.channels['Channel_1'][-1] = diastole1.status - oxi.channels['Channel_2'][-1] = diastole2.status - oxi.channels['Channel_3'][-1] = diastole3.status + oxi.channels["Channel_0"][-1] = beat.status + oxi.channels["Channel_1"][-1] = diastole1.status + oxi.channels["Channel_2"][-1] = diastole2.status + oxi.channels["Channel_3"][-1] = diastole3.status #%% # Events @@ -129,11 +127,12 @@ angles = [] x = np.asarray(oxi.peaks) for ev in oxi.channels: - events = norm_triggers(np.asarray(oxi.channels[ev]), threshold=1, n=40, - direction='higher') + events = norm_triggers( + np.asarray(oxi.channels[ev]), threshold=1, n=40, direction="higher" + ) angles.append(to_angles(np.where(x)[0], np.where(events)[0])) -palette = itertools.cycle(sns.color_palette('deep')) +palette = itertools.cycle(sns.color_palette("deep")) ax = plt.subplot(111, polar=True) for i in angles: circular(i, color=next(palette), ax=ax) diff --git a/examples/plot_InstantaneousHeartRate.py b/examples/plot_InstantaneousHeartRate.py index 664ed6a0..985e0c81 100644 --- a/examples/plot_InstantaneousHeartRate.py +++ b/examples/plot_InstantaneousHeartRate.py @@ -12,12 +12,13 @@ # Author: Nicolas Legrand # Licence: GPL v3 -from systole import serialSim -from systole.utils import heart_rate -from systole.recording import Oximeter import matplotlib.pyplot as plt import numpy as np +from systole import serialSim +from systole.recording import Oximeter +from systole.utils import heart_rate + #%% # Recording # --------- @@ -47,18 +48,22 @@ fig, ax = plt.subplots(3, 1, figsize=(13, 8), sharex=True) oxi.plot_recording(ax=ax[0]) -ax[1].plot(oxi.times, oxi.peaks, 'k') -ax[1].set_title('Peaks vector', fontweight='bold') -ax[1].set_xlabel('Time (s)') -ax[1].set_ylabel('Peak\n detection') +ax[1].plot(oxi.times, oxi.peaks, "k") +ax[1].set_title("Peaks vector", fontweight="bold") +ax[1].set_xlabel("Time (s)") +ax[1].set_ylabel("Peak\n detection") -hr, time = heart_rate(oxi.peaks, sfreq=75, unit='rr', kind='cubic') -ax[2].plot(time, hr, label='Interpolated HR', linestyle='--', color='gray') -ax[2].plot(np.array(oxi.times)[np.where(oxi.peaks)[0]], - hr[np.where(oxi.peaks)[0]], 'ro', label='Instantaneous HR') -ax[2].set_xlabel('Time (s)') -ax[2].set_title('Instantaneous Heart Rate', fontweight='bold') -ax[2].set_ylabel('RR intervals (ms)') +hr, time = heart_rate(oxi.peaks, sfreq=75, unit="rr", kind="cubic") +ax[2].plot(time, hr, label="Interpolated HR", linestyle="--", color="gray") +ax[2].plot( + np.array(oxi.times)[np.where(oxi.peaks)[0]], + hr[np.where(oxi.peaks)[0]], + "ro", + label="Instantaneous HR", +) +ax[2].set_xlabel("Time (s)") +ax[2].set_title("Instantaneous Heart Rate", fontweight="bold") +ax[2].set_ylabel("RR intervals (ms)") plt.tight_layout() diff --git a/examples/plot_InteractiveVisualizations.py b/examples/plot_InteractiveVisualizations.py index b16cb2ce..7d82a967 100644 --- a/examples/plot_InteractiveVisualizations.py +++ b/examples/plot_InteractiveVisualizations.py @@ -14,16 +14,22 @@ #%% import plotly + +from systole import import_ppg, import_rr from systole.detection import rr_artefacts -from systole import import_rr, import_ppg -from systole.plotly import plot_subspaces, plot_frequency, plot_raw,\ - plot_timedomain, plot_nonlinear +from systole.plotly import ( + plot_frequency, + plot_nonlinear, + plot_raw, + plot_subspaces, + plot_timedomain, +) #%% # Raw data # -------- # -ppg = import_ppg()[0] +ppg = import_ppg() plot_raw(ppg) #%% diff --git a/requirements-test.txt b/requirements-test.txt index f612a46e..a84e5939 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,6 +3,7 @@ scipy>=1.3.0 pandas>=0.24 matplotlib>=3.0.2 seaborn>=0.9.0 -plotly>=4.8.0 -plotly_express>=0.4.1 +plotly>=4.14.3 pyserial>=3.4 +tqdm>=4.41.1 +requests>=2.24.0 diff --git a/requirements.txt b/requirements.txt index 5b380c19..aa67be67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ matplotlib>=3.0.2 seaborn>=0.9.0 py-ecg-detectors>=1.0.2 pyserial>=3.4 +outdated \ No newline at end of file diff --git a/setup.py b/setup.py index 1ced8f2d..9724b421 100644 --- a/setup.py +++ b/setup.py @@ -8,45 +8,44 @@ def read(fname): DESCRIPTION = """Psychophysiology with Python""" -DISTNAME = 'systole' -MAINTAINER = 'Nicolas Legrand' -MAINTAINER_EMAIL = 'nicolas.legrand@cfin.au.dk' -VERSION = '0.1.2' +DISTNAME = "systole" +MAINTAINER = "Nicolas Legrand" +MAINTAINER_EMAIL = "nicolas.legrand@cfin.au.dk" +VERSION = "0.1.3" INSTALL_REQUIRES = [ - 'numpy>=1.17', - 'scipy>=1.3', - 'pandas>=0.24', - 'matplotlib>=3.0.2', - 'seaborn>=0.9.0', - 'pyserial>=3.4', - 'py-ecg-detectors==1.0.2' + "numpy>=1.17", + "scipy>=1.3", + "pandas>=0.24", + "matplotlib>=3.0.2", + "seaborn>=0.9.0", + "pyserial>=3.4", + "py-ecg-detectors==1.0.2", ] -PACKAGES = [ - 'systole', - 'systole.datasets' -] +PACKAGES = ["systole", "systole.datasets"] try: from setuptools import setup + _has_setuptools = True except ImportError: from distutils.core import setup if __name__ == "__main__": - setup(name=DISTNAME, - author=MAINTAINER, - author_email=MAINTAINER_EMAIL, - maintainer=MAINTAINER, - maintainer_email=MAINTAINER_EMAIL, - description=DESCRIPTION, - long_description=open('README.rst').read(), - long_description_content_type='text/x-rst', - license='GPL-3.0', - version=VERSION, - install_requires=INSTALL_REQUIRES, - include_package_data=True, - packages=PACKAGES, - ) + setup( + name=DISTNAME, + author=MAINTAINER, + author_email=MAINTAINER_EMAIL, + maintainer=MAINTAINER, + maintainer_email=MAINTAINER_EMAIL, + description=DESCRIPTION, + long_description=open("README.rst").read(), + long_description_content_type="text/x-rst", + license="GPL-3.0", + version=VERSION, + install_requires=INSTALL_REQUIRES, + include_package_data=True, + packages=PACKAGES, + ) diff --git a/source/api.rst b/source/api.rst index 1b4a9b58..d5f8a684 100644 --- a/source/api.rst +++ b/source/api.rst @@ -8,84 +8,121 @@ Functions .. contents:: Table of Contents :depth: 3 +Correction +---------- + +.. currentmodule:: systole.correction + +.. _correction: + +.. autosummary:: + :toctree: generated/correction + + correct_extra + correct_missed + interpolate_bads + correct_rr + correct_peaks + correct_missed_peaks + correct_extra_peaks + Detection --------- +.. currentmodule:: systole.detection + .. _detection: .. autosummary:: - :toctree: generated/ + :toctree: generated/detection - oxi_peaks - ecg_peaks - rr_artefacts - interpolate_clipping + oxi_peaks + ecg_peaks + rr_artefacts + interpolate_clipping Heart Rate Variability ---------------------- +.. currentmodule:: systole.hrv + .. _hrv: .. autosummary:: - :toctree: generated/ + :toctree: generated/hrv - nnX - pnnX - rmssd - time_domain - frequency_domain - nonlinear + nnX + pnnX + rmssd + time_domain + frequency_domain + nonlinear -Plotting +Plotly -------- -.. _plotting: +.. currentmodule:: systole.plotly + +.. _plotly: .. autosummary:: - :toctree: generated/ + :toctree: generated/plotly - plot_hr - plot_events - plot_oximeter - plot_subspaces - circular - plot_circular + plot_raw + plot_ectopic + plot_shortLong + plot_subspaces + plot_frequency + plot_nonlinear + plot_timedomain -Plotly +Plotting -------- -.. _plotly: +.. currentmodule:: systole.plotting + +.. _plotting: .. autosummary:: - :toctree: generated/ + :toctree: generated/plotting - plot_raw - plot_ectopic - plot_shortLong - plot_subspaces + plot_raw + plot_events + plot_oximeter + plot_subspaces + plot_psd + circular + plot_circular Recording --------- +.. currentmodule:: systole.recording + .. _recording: .. autosummary:: - :toctree: generated/ + :toctree: generated/recording - recording.Oximeter - recording.BrainVisionExG + Oximeter + BrainVisionExG + findOximeter Utils ----- +.. currentmodule:: systole.utils + .. _utils: .. autosummary:: - :toctree: generated/ - - norm_triggers - time_shift - heart_rate - to_angles - to_epochs - to_rr + :toctree: generated/utils + + norm_triggers + time_shift + heart_rate + to_angles + to_epochs + simulate_rr + to_neighbour + to_rr diff --git a/source/api/systole.circular.examples b/source/api/systole.circular.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.frequency_domain.examples b/source/api/systole.frequency_domain.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.heart_rate.examples b/source/api/systole.heart_rate.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.interpolate_clipping.examples b/source/api/systole.interpolate_clipping.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.nnX.examples b/source/api/systole.nnX.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.nonlinear.examples b/source/api/systole.nonlinear.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.norm_triggers.examples b/source/api/systole.norm_triggers.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.oxi_peaks.examples b/source/api/systole.oxi_peaks.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.plot_circular.examples b/source/api/systole.plot_circular.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.plot_ectopic.examples b/source/api/systole.plot_ectopic.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.plot_events.examples b/source/api/systole.plot_events.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.plot_hr.examples b/source/api/systole.plot_hr.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.plot_oximeter.examples b/source/api/systole.plot_oximeter.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.plot_raw.examples b/source/api/systole.plot_raw.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.plot_shortLong.examples b/source/api/systole.plot_shortLong.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.plot_subspaces.examples b/source/api/systole.plot_subspaces.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.pnnX.examples b/source/api/systole.pnnX.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.__init__.examples b/source/api/systole.recording.Oximeter.__init__.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.add_paquet.examples b/source/api/systole.recording.Oximeter.add_paquet.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.check.examples b/source/api/systole.recording.Oximeter.check.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.examples b/source/api/systole.recording.Oximeter.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.find_peaks.examples b/source/api/systole.recording.Oximeter.find_peaks.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.plot_events.examples b/source/api/systole.recording.Oximeter.plot_events.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.plot_hr.examples b/source/api/systole.recording.Oximeter.plot_hr.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.plot_recording.examples b/source/api/systole.recording.Oximeter.plot_recording.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.read.examples b/source/api/systole.recording.Oximeter.read.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.readInWaiting.examples b/source/api/systole.recording.Oximeter.readInWaiting.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.save.examples b/source/api/systole.recording.Oximeter.save.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.setup.examples b/source/api/systole.recording.Oximeter.setup.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.recording.Oximeter.waitBeat.examples b/source/api/systole.recording.Oximeter.waitBeat.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.rmssd.examples b/source/api/systole.rmssd.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.rr_artefacts.examples b/source/api/systole.rr_artefacts.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.time_domain.examples b/source/api/systole.time_domain.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.time_shift.examples b/source/api/systole.time_shift.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.to_angles.examples b/source/api/systole.to_angles.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/api/systole.to_epochs.examples b/source/api/systole.to_epochs.examples deleted file mode 100644 index e69de29b..00000000 diff --git a/source/auto_examples/Tutorial_recording.ipynb b/source/auto_examples/Tutorial_recording.ipynb new file mode 100644 index 00000000..fc50630a --- /dev/null +++ b/source/auto_examples/Tutorial_recording.ipynb @@ -0,0 +1,115 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n# Recording PPG signal\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# Author: Nicolas Legrand \n# Licence: GPL v3\n\n# The py:class:systole.recording.Oximeter class can be used to read incoming\n# PPG signal from `Nonin 3012LP Xpod USB pulse oximeter\n# `_ together with the `Nonin 8000SM\n# 'soft-clip' fingertip sensors `_.\n# This function can easily be integrated with other stimulus presentation\n# software lie `PsychoPy `_ to record cardiac\n# activity during psychological experiments, or to synchronize stimulus\n# delivery with cardiac phases (e.g. systole or diastole)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reading\nRecording and plotting your first time-series will only require 5 lines\nof code:\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import time\n\nimport serial\n\nfrom systole.recording import Oximeter\n\nser = serial.Serial(\"COM4\") # Add your USB port here\n\n# Open serial port, initialize and plot recording for Oximeter\noxi = Oximeter(serial=ser).setup().read(duration=10)\n\n# The signal can be directly plotted using built-in functions.\noxi.plot_oximeter()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + ".. figure:: https://github.com/embodied-computation-group/systole/raw/master/Images/recording.png\n :align: center\n#############################################################################\n\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interfacing with PsychoPy\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# * The ``read()`` method will record for a predefined amount of time\n# (specified by the ``duration`` parameter, in seconds). This 'serial mode'\n# is the easiest and most robust method, but it does not allow the execution\n# of other instructions in the meantime.\n\n# Code 1 {}\noximeter.read(duration=10)\n# Code 2 {}\n\n# * The ``readInWaiting()`` method will only read the bytes temporally stored\n# in the USB buffer. For the Nonin device, this represents up to 10 seconds of\n# recording (this procedure should be executed at least one time every 10\n# seconds for a continuous recording). When inserted into a while loop, it can\n# record PPG signal in parallel with other commands.\n\n\ntstart = time.time()\nwhile time.time() - tstart < 10:\n oximeter.readInWaiting()\n # Insert code here {...}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Online detection\nOnline heart beat detection, for cardiac-stimulus synchrony\n\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# Open serial port\nser = serial.Serial(\"COM4\") # Change this value according to your setup\n\n# Create an Oxymeter instance and initialize recording\noxi = Oximeter(serial=ser, sfreq=75, add_channels=4).setup()\n\n# Online peak detection for 10 seconds\ntstart = time.time()\nwhile time.time() - tstart < 10:\n while oxi.serial.inWaiting() >= 5:\n paquet = list(oxi.serial.read(5))\n oxi.add_paquet(paquet[2]) # Add new data point\n if oxi.peaks[-1] == 1:\n print(\"Heartbeat detected\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/source/auto_examples/Tutorial_recording.py b/source/auto_examples/Tutorial_recording.py new file mode 100644 index 00000000..abf0eb52 --- /dev/null +++ b/source/auto_examples/Tutorial_recording.py @@ -0,0 +1,88 @@ +""" +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 time + +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. + + +tstart = time.time() +while time.time() - tstart < 10: + oximeter.readInWaiting() + # Insert code here {...} + +#%% +# Online detection +# ---------------- +# Online heart beat detection, for cardiac-stimulus synchrony + + +# 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/source/auto_examples/Tutorial_recording.rst b/source/auto_examples/Tutorial_recording.rst new file mode 100644 index 00000000..6418269a --- /dev/null +++ b/source/auto_examples/Tutorial_recording.rst @@ -0,0 +1,166 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples\Tutorial_recording.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + Click :ref:`here ` + to download the full example code + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_Tutorial_recording.py: + + +Recording PPG signal +==================== + +.. GENERATED FROM PYTHON SOURCE LINES 5-19 + +.. code-block:: default + + + # 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). + + + +.. GENERATED FROM PYTHON SOURCE LINES 20-24 + +Reading +------- +Recording and plotting your first time-series will only require 5 lines +of code: + +.. GENERATED FROM PYTHON SOURCE LINES 24-39 + +.. code-block:: default + + + import time + + 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() + + +.. GENERATED FROM PYTHON SOURCE LINES 40-43 + +.. figure:: https://github.com/embodied-computation-group/systole/raw/master/Images/recording.png + :align: center +############################################################################# + +.. GENERATED FROM PYTHON SOURCE LINES 45-47 + +Interfacing with PsychoPy +------------------------- + +.. GENERATED FROM PYTHON SOURCE LINES 47-69 + +.. code-block:: default + + + # * 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. + + + tstart = time.time() + while time.time() - tstart < 10: + oximeter.readInWaiting() + # Insert code here {...} + + +.. GENERATED FROM PYTHON SOURCE LINES 70-73 + +Online detection +---------------- +Online heart beat detection, for cardiac-stimulus synchrony + +.. GENERATED FROM PYTHON SOURCE LINES 73-89 + +.. code-block:: default + + + + # 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") + + +.. rst-class:: sphx-glr-timing + + **Total running time of the script:** ( 0 minutes 0.000 seconds) + + +.. _sphx_glr_download_auto_examples_Tutorial_recording.py: + + +.. only :: html + + .. container:: sphx-glr-footer + :class: sphx-glr-footer-example + + + + .. container:: sphx-glr-download sphx-glr-download-python + + :download:`Download Python source code: Tutorial_recording.py ` + + + + .. container:: sphx-glr-download sphx-glr-download-jupyter + + :download:`Download Jupyter notebook: Tutorial_recording.ipynb ` + + +.. only:: html + + .. rst-class:: sphx-glr-signature + + `Gallery generated by Sphinx-Gallery `_ diff --git a/source/auto_examples/Tutorial_recording_codeobj.pickle b/source/auto_examples/Tutorial_recording_codeobj.pickle new file mode 100644 index 00000000..df24569b Binary files /dev/null and b/source/auto_examples/Tutorial_recording_codeobj.pickle differ diff --git a/source/auto_examples/auto_examples_jupyter.zip b/source/auto_examples/auto_examples_jupyter.zip index bb9fb6f7..aba6089f 100644 Binary files a/source/auto_examples/auto_examples_jupyter.zip and b/source/auto_examples/auto_examples_jupyter.zip differ diff --git a/source/auto_examples/auto_examples_python.zip b/source/auto_examples/auto_examples_python.zip index ab9c09ae..db6076c0 100644 Binary files a/source/auto_examples/auto_examples_python.zip and b/source/auto_examples/auto_examples_python.zip differ diff --git a/source/auto_examples/images/sphx_glr_plot_ArtefactsDetection_001.png b/source/auto_examples/images/sphx_glr_plot_ArtefactsDetection_001.png index ff7a3eae..b60994f3 100644 Binary files a/source/auto_examples/images/sphx_glr_plot_ArtefactsDetection_001.png and b/source/auto_examples/images/sphx_glr_plot_ArtefactsDetection_001.png differ diff --git a/source/auto_examples/images/sphx_glr_plot_ECGProcessing_001.png b/source/auto_examples/images/sphx_glr_plot_ECGProcessing_001.png new file mode 100644 index 00000000..276e5200 Binary files /dev/null and b/source/auto_examples/images/sphx_glr_plot_ECGProcessing_001.png differ diff --git a/source/auto_examples/images/sphx_glr_plot_HeartBeatEvokedArpeggios_001.png b/source/auto_examples/images/sphx_glr_plot_HeartBeatEvokedArpeggios_001.png index b9717ace..b1948478 100644 Binary files a/source/auto_examples/images/sphx_glr_plot_HeartBeatEvokedArpeggios_001.png and b/source/auto_examples/images/sphx_glr_plot_HeartBeatEvokedArpeggios_001.png differ diff --git a/source/auto_examples/images/sphx_glr_plot_HeartBeatEvokedArpeggios_002.png b/source/auto_examples/images/sphx_glr_plot_HeartBeatEvokedArpeggios_002.png index c9901863..2d6b3aa8 100644 Binary files a/source/auto_examples/images/sphx_glr_plot_HeartBeatEvokedArpeggios_002.png and b/source/auto_examples/images/sphx_glr_plot_HeartBeatEvokedArpeggios_002.png differ diff --git a/source/auto_examples/images/sphx_glr_plot_InstantaneousHeartRate_001.png b/source/auto_examples/images/sphx_glr_plot_InstantaneousHeartRate_001.png index ba4db072..2e314b12 100644 Binary files a/source/auto_examples/images/sphx_glr_plot_InstantaneousHeartRate_001.png and b/source/auto_examples/images/sphx_glr_plot_InstantaneousHeartRate_001.png differ diff --git a/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_001.html b/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_001.html index ccff5468..cb375600 100644 --- a/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_001.html +++ b/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_001.html @@ -1,85 +1,7 @@ -
- - - -
- -
+
+
\ No newline at end of file diff --git a/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_002.html b/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_002.html index 1c9498bc..042e6a86 100644 --- a/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_002.html +++ b/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_002.html @@ -1,85 +1,7 @@ -
- - - -
- -
+
+
\ No newline at end of file diff --git a/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_003.html b/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_003.html index acc2b08e..1c940d56 100644 --- a/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_003.html +++ b/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_003.html @@ -1,85 +1,7 @@ -
- - - -
- -
+
+
\ No newline at end of file diff --git a/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_003.png b/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_003.png index 7f8c7156..26f7f437 100644 Binary files a/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_003.png and b/source/auto_examples/images/sphx_glr_plot_InteractiveVisualizations_003.png differ diff --git a/source/auto_examples/images/thumb/sphx_glr_Tutorial_HRV_thumb.png b/source/auto_examples/images/thumb/sphx_glr_Tutorial_HRV_thumb.png new file mode 100644 index 00000000..233f8e60 Binary files /dev/null and b/source/auto_examples/images/thumb/sphx_glr_Tutorial_HRV_thumb.png differ diff --git a/source/auto_examples/images/thumb/sphx_glr_Tutorial_recording_thumb.png b/source/auto_examples/images/thumb/sphx_glr_Tutorial_recording_thumb.png new file mode 100644 index 00000000..233f8e60 Binary files /dev/null and b/source/auto_examples/images/thumb/sphx_glr_Tutorial_recording_thumb.png differ diff --git a/source/auto_examples/images/thumb/sphx_glr_plot_ArtefactsDetection_thumb.png b/source/auto_examples/images/thumb/sphx_glr_plot_ArtefactsDetection_thumb.png index 05d3099a..117a0469 100644 Binary files a/source/auto_examples/images/thumb/sphx_glr_plot_ArtefactsDetection_thumb.png and b/source/auto_examples/images/thumb/sphx_glr_plot_ArtefactsDetection_thumb.png differ diff --git a/source/auto_examples/images/thumb/sphx_glr_plot_ECGProcessing_thumb.png b/source/auto_examples/images/thumb/sphx_glr_plot_ECGProcessing_thumb.png new file mode 100644 index 00000000..b06c4e6a Binary files /dev/null and b/source/auto_examples/images/thumb/sphx_glr_plot_ECGProcessing_thumb.png differ diff --git a/source/auto_examples/images/thumb/sphx_glr_plot_HeartBeatEvokedArpeggios_thumb.png b/source/auto_examples/images/thumb/sphx_glr_plot_HeartBeatEvokedArpeggios_thumb.png index 6c1c76df..91a304bd 100644 Binary files a/source/auto_examples/images/thumb/sphx_glr_plot_HeartBeatEvokedArpeggios_thumb.png and b/source/auto_examples/images/thumb/sphx_glr_plot_HeartBeatEvokedArpeggios_thumb.png differ diff --git a/source/auto_examples/images/thumb/sphx_glr_plot_InstantaneousHeartRate_thumb.png b/source/auto_examples/images/thumb/sphx_glr_plot_InstantaneousHeartRate_thumb.png index 9166de71..f8f17257 100644 Binary files a/source/auto_examples/images/thumb/sphx_glr_plot_InstantaneousHeartRate_thumb.png and b/source/auto_examples/images/thumb/sphx_glr_plot_InstantaneousHeartRate_thumb.png differ diff --git a/source/auto_examples/images/thumb/sphx_glr_plot_RespirationFromPPG_thumb.png b/source/auto_examples/images/thumb/sphx_glr_plot_RespirationFromPPG_thumb.png new file mode 100644 index 00000000..b06c4e6a Binary files /dev/null and b/source/auto_examples/images/thumb/sphx_glr_plot_RespirationFromPPG_thumb.png differ diff --git a/source/auto_examples/index.rst b/source/auto_examples/index.rst index 01dbd81e..09be4e39 100644 --- a/source/auto_examples/index.rst +++ b/source/auto_examples/index.rst @@ -102,6 +102,27 @@ If you want to see the tutorials in action, you can also click on the link below /auto_examples/plot_InstantaneousHeartRate +.. raw:: html + +
+ +.. only:: html + + .. figure:: /auto_examples/images/thumb/sphx_glr_Tutorial_recording_thumb.png + :alt: Recording PPG signal + + :ref:`sphx_glr_auto_examples_Tutorial_recording.py` + +.. raw:: html + +
+ + +.. toctree:: + :hidden: + + /auto_examples/Tutorial_recording + .. raw:: html
@@ -136,13 +157,13 @@ If you want to see the tutorials in action, you can also click on the link below .. container:: sphx-glr-download sphx-glr-download-python - :download:`Download all examples in Python source code: auto_examples_python.zip ` + :download:`Download all examples in Python source code: auto_examples_python.zip ` .. container:: sphx-glr-download sphx-glr-download-jupyter - :download:`Download all examples in Jupyter notebooks: auto_examples_jupyter.zip ` + :download:`Download all examples in Jupyter notebooks: auto_examples_jupyter.zip ` .. only:: html diff --git a/source/auto_examples/plot_ArtefactsCorrection.ipynb b/source/auto_examples/plot_ArtefactsCorrection.ipynb index c238176b..ac2c3ed4 100644 --- a/source/auto_examples/plot_ArtefactsCorrection.ipynb +++ b/source/auto_examples/plot_ArtefactsCorrection.ipynb @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\nOutliers and ectobeats correction\n=================================\n\nHere, we describe two method for artefacts and outliers correction, after\ndetection using the method proposed by Lipponen & Tarvainen (2019) [#]_.\n" + "\n# Outliers and ectobeats correction\n\nHere, we describe two method for artefacts and outliers correction, after\ndetection using the method proposed by Lipponen & Tarvainen (2019) [#]_.\n" ] }, { @@ -26,7 +26,7 @@ }, "outputs": [], "source": [ - "# Author: Nicolas Legrand \n# Licence: GPL v3\n\n# Two approaches for artefacts correction are proposed:\n# * `correct_peaks()` will find and correct artefacts in a boolean peaks\n# vector, thus ensuring the length of recording remain constant and corrected\n# peaks fit the signal sampling rate. This method is more adapted to\n# event-related cardiac activity designs.\n\n# * `correct_rr()` will find and correct artefacts in the RR time series. The\n# signal length will possibly change after the interpolation of long, short or\n# ectopic beats. This method is more relevant for HRV analyse of long recording\n# where the timing of experimental events is not important." + "# Author: Nicolas Legrand \n# Licence: GPL v3\n\n# Two approaches for artefacts correction are proposed:\n# * `correct_peaks()` will find and correct artefacts in a boolean peaks\n# vector, thus ensuring the length of recording remain constant and corrected\n# peaks fit the signal sampling rate. This method is more adapted to\n# event-related cardiac activity designs.\n\n# * `correct_rr()` will find and correct artefacts in the RR time series. The\n# signal length will possibly change after the interpolation of long, short or\n# ectopic beats. This method is more relevant for HRV analyse of long recording\n# where the timing of experimental events is not important.\n\nimport matplotlib.pyplot as plt" ] }, { @@ -37,7 +37,7 @@ }, "outputs": [], "source": [ - "import numpy as np\nimport matplotlib.pyplot as plt\nfrom systole import simulate_rr\nfrom systole.plotting import plot_subspaces\nfrom systole.correction import correct_peaks, correct_rr" + "import numpy as np\n\nfrom systole import simulate_rr\nfrom systole.correction import correct_peaks, correct_rr\nfrom systole.plotting import plot_subspaces" ] }, { @@ -80,7 +80,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "References\n----------\n.. [#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for\n heart rate variability time series artefact correction using novel\n beat classification. Journal of Medical Engineering & Technology,\n 43(3), 173\u2013181. https://doi.org/10.1080/03091902.2019.1640306\n\n" + "## References\n.. [#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for\n heart rate variability time series artefact correction using novel\n beat classification. Journal of Medical Engineering & Technology,\n 43(3), 173\u2013181. https://doi.org/10.1080/03091902.2019.1640306\n\n" ] } ], @@ -100,7 +100,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.7.9" } }, "nbformat": 4, diff --git a/source/auto_examples/plot_ArtefactsCorrection.py b/source/auto_examples/plot_ArtefactsCorrection.py index 23081216..86fe1b80 100644 --- a/source/auto_examples/plot_ArtefactsCorrection.py +++ b/source/auto_examples/plot_ArtefactsCorrection.py @@ -20,13 +20,13 @@ # ectopic beats. This method is more relevant for HRV analyse of long recording # where the timing of experimental events is not important. +import matplotlib.pyplot as plt #%% import numpy as np -import matplotlib.pyplot as plt + from systole import simulate_rr -from systole.plotting import plot_subspaces from systole.correction import correct_peaks, correct_rr - +from systole.plotting import plot_subspaces #%% Method 1 - Peaks correction # ############################# diff --git a/source/auto_examples/plot_ArtefactsCorrection.py.md5 b/source/auto_examples/plot_ArtefactsCorrection.py.md5 index 6d0f4c05..48fd0b2b 100644 --- a/source/auto_examples/plot_ArtefactsCorrection.py.md5 +++ b/source/auto_examples/plot_ArtefactsCorrection.py.md5 @@ -1 +1 @@ -b249ea1af60fe433eb0565a2693898a6 \ No newline at end of file +3aef4defe652fe63c7a1eda1c7f030b5 \ No newline at end of file diff --git a/source/auto_examples/plot_ArtefactsCorrection.rst b/source/auto_examples/plot_ArtefactsCorrection.rst index e22c6f93..e5831943 100644 --- a/source/auto_examples/plot_ArtefactsCorrection.rst +++ b/source/auto_examples/plot_ArtefactsCorrection.rst @@ -1,12 +1,21 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples\plot_ArtefactsCorrection.py" +.. LINE NUMBERS ARE GIVEN BELOW. + .. only:: html .. note:: :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code - .. rst-class:: sphx-glr-example-title + Click :ref:`here ` + to download the full example code + +.. rst-class:: sphx-glr-example-title - .. _sphx_glr_auto_examples_plot_ArtefactsCorrection.py: +.. _sphx_glr_auto_examples_plot_ArtefactsCorrection.py: Outliers and ectobeats correction @@ -15,6 +24,7 @@ Outliers and ectobeats correction Here, we describe two method for artefacts and outliers correction, after detection using the method proposed by Lipponen & Tarvainen (2019) [#]_. +.. GENERATED FROM PYTHON SOURCE LINES 8-24 .. code-block:: default @@ -33,6 +43,7 @@ detection using the method proposed by Lipponen & Tarvainen (2019) [#]_. # ectopic beats. This method is more relevant for HRV analyse of long recording # where the timing of experimental events is not important. + import matplotlib.pyplot as plt @@ -40,14 +51,15 @@ detection using the method proposed by Lipponen & Tarvainen (2019) [#]_. +.. GENERATED FROM PYTHON SOURCE LINES 25-31 .. code-block:: default import numpy as np - import matplotlib.pyplot as plt + from systole import simulate_rr - from systole.plotting import plot_subspaces from systole.correction import correct_peaks, correct_rr + from systole.plotting import plot_subspaces @@ -56,9 +68,11 @@ detection using the method proposed by Lipponen & Tarvainen (2019) [#]_. +.. GENERATED FROM PYTHON SOURCE LINES 32-33 ############################# +.. GENERATED FROM PYTHON SOURCE LINES 33-38 .. code-block:: default @@ -82,8 +96,11 @@ detection using the method proposed by Lipponen & Tarvainen (2019) [#]_. +.. GENERATED FROM PYTHON SOURCE LINES 39-40 + ############################# +.. GENERATED FROM PYTHON SOURCE LINES 40-43 .. code-block:: default @@ -97,6 +114,8 @@ detection using the method proposed by Lipponen & Tarvainen (2019) [#]_. +.. GENERATED FROM PYTHON SOURCE LINES 44-50 + References ---------- .. [#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for @@ -107,7 +126,7 @@ References .. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 0.493 seconds) + **Total running time of the script:** ( 0 minutes 0.132 seconds) .. _sphx_glr_download_auto_examples_plot_ArtefactsCorrection.py: diff --git a/source/auto_examples/plot_ArtefactsCorrection_codeobj.pickle b/source/auto_examples/plot_ArtefactsCorrection_codeobj.pickle index 3c030986..c60e91a0 100644 Binary files a/source/auto_examples/plot_ArtefactsCorrection_codeobj.pickle and b/source/auto_examples/plot_ArtefactsCorrection_codeobj.pickle differ diff --git a/source/auto_examples/plot_ArtefactsDetection.ipynb b/source/auto_examples/plot_ArtefactsDetection.ipynb index 61b9538e..5b08500f 100644 --- a/source/auto_examples/plot_ArtefactsDetection.ipynb +++ b/source/auto_examples/plot_ArtefactsDetection.ipynb @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\nOutliers and artefacts detection\n================================\n\nThis example shows how to detect ectopic, missed, extra, slow and long long\nfrom RR or pulse rate interval time series using the method proposed by\nLipponen & Tarvainen (2019) [#]_.\n" + "\n# Outliers and artefacts detection\n\nThis example shows how to detect ectopic, missed, extra, slow and long long\nfrom RR or pulse rate interval time series using the method proposed by\nLipponen & Tarvainen (2019) [#]_.\n" ] }, { @@ -37,21 +37,21 @@ }, "outputs": [], "source": [ - "from systole.detection import rr_artefacts\nfrom systole.utils import simulate_rr\nfrom systole.plotting import plot_subspaces" + "from systole.detection import rr_artefacts\nfrom systole.plotting import plot_subspaces\nfrom systole.utils import simulate_rr" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "RR artefacts\n------------\nThe proposed method will detect 4 kinds of artefacts in an RR time series:\nMissed R peaks, when an existing R component was erroneously NOT detected by\nthe algorithm.\n* Extra R peaks, when an R peak was detected but does not exist in the\nsignal.\n* Long or short interval intervals, when R peaks are correctly detected but\nthe resulting interval has extreme value in the overall time-series.\n* Ectopic beats, due to disturbance of the cardiac rhythm when the heart\neither skip or add an extra beat.\n* The category in which the artefact belongs will have an influence on the\ncorrection procedure (see Artefact correction).\n\n" + "## RR artefacts\nThe proposed method will detect 4 kinds of artefacts in an RR time series:\nMissed R peaks, when an existing R component was erroneously NOT detected by\nthe algorithm.\n* Extra R peaks, when an R peak was detected but does not exist in the\nsignal.\n* Long or short interval intervals, when R peaks are correctly detected but\nthe resulting interval has extreme value in the overall time-series.\n* Ectopic beats, due to disturbance of the cardiac rhythm when the heart\neither skip or add an extra beat.\n* The category in which the artefact belongs will have an influence on the\ncorrection procedure (see Artefact correction).\n\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Simulate RR time series\n-----------------------\nThis function will simulate RR time series containing ectopic, extra, missed,\nlong and short artefacts.\n\n" + "## Simulate RR time series\nThis function will simulate RR time series containing ectopic, extra, missed,\nlong and short artefacts.\n\n" ] }, { @@ -69,7 +69,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Artefact detection\n------------------\n\n" + "## Artefact detection\n\n" ] }, { @@ -87,7 +87,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Subspaces visualization\n-----------------------\nYou can visualize the two main subspaces and spot outliers. The left pamel\nplot subspaces that are more sensitive to ectopic beats detection. The right\npanel plot subspaces that will be more sensitive to long or short beats,\ncomprizing the extra and missed beats.\n\n" + "## Subspaces visualization\nYou can visualize the two main subspaces and spot outliers. The left pamel\nplot subspaces that are more sensitive to ectopic beats detection. The right\npanel plot subspaces that will be more sensitive to long or short beats,\ncomprizing the extra and missed beats.\n\n" ] }, { @@ -105,7 +105,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "References\n----------\n.. [#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for\n heart rate variability time series artefact correction using novel\n beat classification. Journal of Medical Engineering & Technology,\n 43(3), 173\u2013181. https://doi.org/10.1080/03091902.2019.1640306\n\n" + "## References\n.. [#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for\n heart rate variability time series artefact correction using novel\n beat classification. Journal of Medical Engineering & Technology,\n 43(3), 173\u2013181. https://doi.org/10.1080/03091902.2019.1640306\n\n" ] } ], @@ -125,7 +125,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.7.9" } }, "nbformat": 4, diff --git a/source/auto_examples/plot_ArtefactsDetection.py b/source/auto_examples/plot_ArtefactsDetection.py index 71f6e97b..0f955bcd 100644 --- a/source/auto_examples/plot_ArtefactsDetection.py +++ b/source/auto_examples/plot_ArtefactsDetection.py @@ -12,8 +12,8 @@ #%% from systole.detection import rr_artefacts -from systole.utils import simulate_rr from systole.plotting import plot_subspaces +from systole.utils import simulate_rr #%% # RR artefacts diff --git a/source/auto_examples/plot_ArtefactsDetection.py.md5 b/source/auto_examples/plot_ArtefactsDetection.py.md5 index f88ec5d6..07e2582d 100644 --- a/source/auto_examples/plot_ArtefactsDetection.py.md5 +++ b/source/auto_examples/plot_ArtefactsDetection.py.md5 @@ -1 +1 @@ -a2452a9bf97fecafbff193fcb02361fa \ No newline at end of file +4e7dc2a699048ce938dac18e50c4acef \ No newline at end of file diff --git a/source/auto_examples/plot_ArtefactsDetection.rst b/source/auto_examples/plot_ArtefactsDetection.rst index 62e233e3..87661ff2 100644 --- a/source/auto_examples/plot_ArtefactsDetection.rst +++ b/source/auto_examples/plot_ArtefactsDetection.rst @@ -1,12 +1,21 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples\plot_ArtefactsDetection.py" +.. LINE NUMBERS ARE GIVEN BELOW. + .. only:: html .. note:: :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code - .. rst-class:: sphx-glr-example-title + Click :ref:`here ` + to download the full example code + +.. rst-class:: sphx-glr-example-title - .. _sphx_glr_auto_examples_plot_ArtefactsDetection.py: +.. _sphx_glr_auto_examples_plot_ArtefactsDetection.py: Outliers and artefacts detection @@ -16,6 +25,7 @@ This example shows how to detect ectopic, missed, extra, slow and long long from RR or pulse rate interval time series using the method proposed by Lipponen & Tarvainen (2019) [#]_. +.. GENERATED FROM PYTHON SOURCE LINES 9-13 .. code-block:: default @@ -30,12 +40,13 @@ Lipponen & Tarvainen (2019) [#]_. +.. GENERATED FROM PYTHON SOURCE LINES 14-18 .. code-block:: default from systole.detection import rr_artefacts - from systole.utils import simulate_rr from systole.plotting import plot_subspaces + from systole.utils import simulate_rr @@ -44,6 +55,8 @@ Lipponen & Tarvainen (2019) [#]_. +.. GENERATED FROM PYTHON SOURCE LINES 19-32 + RR artefacts ------------ The proposed method will detect 4 kinds of artefacts in an RR time series: @@ -58,11 +71,14 @@ either skip or add an extra beat. * The category in which the artefact belongs will have an influence on the correction procedure (see Artefact correction). +.. GENERATED FROM PYTHON SOURCE LINES 34-38 + Simulate RR time series ----------------------- This function will simulate RR time series containing ectopic, extra, missed, long and short artefacts. +.. GENERATED FROM PYTHON SOURCE LINES 38-41 .. code-block:: default @@ -76,9 +92,12 @@ long and short artefacts. +.. GENERATED FROM PYTHON SOURCE LINES 42-44 + Artefact detection ------------------ +.. GENERATED FROM PYTHON SOURCE LINES 44-47 .. code-block:: default @@ -92,6 +111,8 @@ Artefact detection +.. GENERATED FROM PYTHON SOURCE LINES 48-54 + Subspaces visualization ----------------------- You can visualize the two main subspaces and spot outliers. The left pamel @@ -99,6 +120,7 @@ plot subspaces that are more sensitive to ectopic beats detection. The right panel plot subspaces that will be more sensitive to long or short beats, comprizing the extra and missed beats. +.. GENERATED FROM PYTHON SOURCE LINES 54-57 .. code-block:: default @@ -120,12 +142,14 @@ comprizing the extra and missed beats. .. code-block:: none - array([, - ], + array([, + ], dtype=object) +.. GENERATED FROM PYTHON SOURCE LINES 58-64 + References ---------- .. [#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for @@ -136,7 +160,7 @@ References .. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 0.901 seconds) + **Total running time of the script:** ( 0 minutes 1.520 seconds) .. _sphx_glr_download_auto_examples_plot_ArtefactsDetection.py: diff --git a/source/auto_examples/plot_ArtefactsDetection_codeobj.pickle b/source/auto_examples/plot_ArtefactsDetection_codeobj.pickle index bac4dda8..ca140d90 100644 Binary files a/source/auto_examples/plot_ArtefactsDetection_codeobj.pickle and b/source/auto_examples/plot_ArtefactsDetection_codeobj.pickle differ diff --git a/source/auto_examples/plot_HeartBeatEvokedArpeggios.ipynb b/source/auto_examples/plot_HeartBeatEvokedArpeggios.ipynb index 5a824167..2cb1fc7b 100644 --- a/source/auto_examples/plot_HeartBeatEvokedArpeggios.ipynb +++ b/source/auto_examples/plot_HeartBeatEvokedArpeggios.ipynb @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\nHeartbeat Evoked Arpeggios\n============================\n\nThis tutorial illustrates how to use the ``Oximeter`` class to trigger stimuli\nat different phases of the cardiac cycle using the [Psychopy](https://www.psychopy.org/)\ntoolbox. The PPG signal is recorded for 30 seconds and peaks are detected\nonline. Four notes ('C', 'E', 'G', 'Bfl') are played in synch with peak\ndetection with various delays: no delay, 1/4, 2/4 or 3/4 of the previous\ncardiac cycle length. While R-R intervals are prone to large changes over longer\ntimescales, such changes are physiologically limited from one heartbeat to the next,\nlimiting variance in the onset synchrony between the tones and the cardiac cycle.\nOn this basis, each presentation time is calibrated based on the previous RR-interval.\nThis procedure can easily be adapted to create a standard interoception task, e.g. by either presenting\ntones at no delay (systole, s+) or at a fixed offset (diastole, s-).\n" + "\n# Heartbeat Evoked Arpeggios\n\nThis tutorial illustrates how to use the ``Oximeter`` class to trigger stimuli\nat different phases of the cardiac cycle using the [Psychopy](https://www.psychopy.org/)\ntoolbox. The PPG signal is recorded for 30 seconds and peaks are detected\nonline. Four notes ('C', 'E', 'G', 'Bfl') are played in synch with peak\ndetection with various delays: no delay, 1/4, 2/4 or 3/4 of the previous\ncardiac cycle length. While R-R intervals are prone to large changes over longer\ntimescales, such changes are physiologically limited from one heartbeat to the next,\nlimiting variance in the onset synchrony between the tones and the cardiac cycle.\nOn this basis, each presentation time is calibrated based on the previous RR-interval.\nThis procedure can easily be adapted to create a standard interoception task, e.g. by either presenting\ntones at no delay (systole, s+) or at a fixed offset (diastole, s-).\n" ] }, { @@ -26,14 +26,14 @@ }, "outputs": [], "source": [ - "# Author: Nicolas Legrand \n# Licence: GPL v3\n\nimport time\nimport numpy as np\nimport itertools\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nfrom psychopy.sound import Sound\n\nfrom systole.utils import norm_triggers\nfrom systole import serialSim\nfrom systole.utils import to_angles\nfrom systole.plotting import circular\nfrom systole.recording import Oximeter" + "# Author: Nicolas Legrand \n# Licence: GPL v3\n\nimport itertools\nimport time\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport seaborn as sns\nfrom psychopy.sound import Sound\n\nfrom systole import serialSim\nfrom systole.plotting import circular\nfrom systole.recording import Oximeter\nfrom systole.utils import norm_triggers, to_angles" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Recording\n---------\nFor the purpose of demonstration, here we simulate data acquisition through\nthe pulse oximeter using pre-recorded signal.\n\n" + "## Recording\nFor the purpose of demonstration, here we simulate data acquisition through\nthe pulse oximeter using pre-recorded signal.\n\n" ] }, { @@ -94,14 +94,14 @@ }, "outputs": [], "source": [ - "beat = Sound('C', secs=0.1)\ndiastole1 = Sound('E', secs=0.1)\ndiastole2 = Sound('G', secs=0.1)\ndiastole3 = Sound('Bfl', secs=0.1)\n\nsystoleTime1, systoleTime2, systoleTime3 = None, None, None\ntstart = time.time()\nwhile time.time() - tstart < 30:\n\n # Check if there are new data to read\n while oxi.serial.inWaiting() >= 5:\n\n # Convert bytes into list of int\n paquet = list(oxi.serial.read(5))\n\n if oxi.check(paquet): # Data consistency\n oxi.add_paquet(paquet[2]) # Add new data point\n\n # T + 0\n if oxi.peaks[-1] == 1:\n beat = Sound('C', secs=0.1)\n beat.play()\n systoleTime1 = time.time()\n systoleTime2 = time.time()\n systoleTime3 = time.time()\n\n # T + 1/4\n if systoleTime1 is not None:\n if time.time() - systoleTime1 >= ((oxi.instant_rr[-1]/4)/1000):\n diastole1 = Sound('E', secs=0.1)\n diastole1.play()\n systoleTime1 = None\n\n # T + 2/4\n if systoleTime2 is not None:\n if time.time() - systoleTime2 >= (\n ((oxi.instant_rr[-1]/4) * 2)/1000):\n diastole2 = Sound('G', secs=0.1)\n diastole2.play()\n systoleTime2 = None\n\n # T + 3/4\n if systoleTime3 is not None:\n if time.time() - systoleTime3 >= (\n ((oxi.instant_rr[-1]/4) * 3)/1000):\n diastole3 = Sound('A', secs=0.1)\n diastole3.play()\n systoleTime3 = None\n\n # Track the note status\n oxi.channels['Channel_0'][-1] = beat.status\n oxi.channels['Channel_1'][-1] = diastole1.status\n oxi.channels['Channel_2'][-1] = diastole2.status\n oxi.channels['Channel_3'][-1] = diastole3.status" + "beat = Sound(\"C\", secs=0.1)\ndiastole1 = Sound(\"E\", secs=0.1)\ndiastole2 = Sound(\"G\", secs=0.1)\ndiastole3 = Sound(\"Bfl\", secs=0.1)\n\nsystoleTime1, systoleTime2, systoleTime3 = None, None, None\ntstart = time.time()\nwhile time.time() - tstart < 30:\n\n # Check if there are new data to read\n while oxi.serial.inWaiting() >= 5:\n\n # Convert bytes into list of int\n paquet = list(oxi.serial.read(5))\n\n if oxi.check(paquet): # Data consistency\n oxi.add_paquet(paquet[2]) # Add new data point\n\n # T + 0\n if oxi.peaks[-1] == 1:\n beat = Sound(\"C\", secs=0.1)\n beat.play()\n systoleTime1 = time.time()\n systoleTime2 = time.time()\n systoleTime3 = time.time()\n\n # T + 1/4\n if systoleTime1 is not None:\n if time.time() - systoleTime1 >= ((oxi.instant_rr[-1] / 4) / 1000):\n diastole1 = Sound(\"E\", secs=0.1)\n diastole1.play()\n systoleTime1 = None\n\n # T + 2/4\n if systoleTime2 is not None:\n if time.time() - systoleTime2 >= (((oxi.instant_rr[-1] / 4) * 2) / 1000):\n diastole2 = Sound(\"G\", secs=0.1)\n diastole2.play()\n systoleTime2 = None\n\n # T + 3/4\n if systoleTime3 is not None:\n if time.time() - systoleTime3 >= (((oxi.instant_rr[-1] / 4) * 3) / 1000):\n diastole3 = Sound(\"A\", secs=0.1)\n diastole3.play()\n systoleTime3 = None\n\n # Track the note status\n oxi.channels[\"Channel_0\"][-1] = beat.status\n oxi.channels[\"Channel_1\"][-1] = diastole1.status\n oxi.channels[\"Channel_2\"][-1] = diastole2.status\n oxi.channels[\"Channel_3\"][-1] = diastole3.status" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Events\n--------\n\n" + "## Events\n\n" ] }, { @@ -119,7 +119,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Cardiac cycle\n-------------\n\n" + "## Cardiac cycle\n\n" ] }, { @@ -130,7 +130,7 @@ }, "outputs": [], "source": [ - "angles = []\nx = np.asarray(oxi.peaks)\nfor ev in oxi.channels:\n events = norm_triggers(np.asarray(oxi.channels[ev]), threshold=1, n=40,\n direction='higher')\n angles.append(to_angles(np.where(x)[0], np.where(events)[0]))\n\npalette = itertools.cycle(sns.color_palette('deep'))\nax = plt.subplot(111, polar=True)\nfor i in angles:\n circular(i, color=next(palette), ax=ax)" + "angles = []\nx = np.asarray(oxi.peaks)\nfor ev in oxi.channels:\n events = norm_triggers(\n np.asarray(oxi.channels[ev]), threshold=1, n=40, direction=\"higher\"\n )\n angles.append(to_angles(np.where(x)[0], np.where(events)[0]))\n\npalette = itertools.cycle(sns.color_palette(\"deep\"))\nax = plt.subplot(111, polar=True)\nfor i in angles:\n circular(i, color=next(palette), ax=ax)" ] } ], @@ -150,7 +150,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.7.9" } }, "nbformat": 4, diff --git a/source/auto_examples/plot_HeartBeatEvokedArpeggios.py b/source/auto_examples/plot_HeartBeatEvokedArpeggios.py index 7121a50e..af7981e1 100644 --- a/source/auto_examples/plot_HeartBeatEvokedArpeggios.py +++ b/source/auto_examples/plot_HeartBeatEvokedArpeggios.py @@ -18,18 +18,18 @@ # Author: Nicolas Legrand # Licence: GPL v3 +import itertools import time + +import matplotlib.pyplot as plt import numpy as np -import itertools import seaborn as sns -import matplotlib.pyplot as plt from psychopy.sound import Sound -from systole.utils import norm_triggers from systole import serialSim -from systole.utils import to_angles from systole.plotting import circular from systole.recording import Oximeter +from systole.utils import norm_triggers, to_angles #%% # Recording @@ -58,10 +58,10 @@ #%% # Create an Oxymeter instance, initialize recording and record for 10 seconds -beat = Sound('C', secs=0.1) -diastole1 = Sound('E', secs=0.1) -diastole2 = Sound('G', secs=0.1) -diastole3 = Sound('Bfl', secs=0.1) +beat = Sound("C", secs=0.1) +diastole1 = Sound("E", secs=0.1) +diastole2 = Sound("G", secs=0.1) +diastole3 = Sound("Bfl", secs=0.1) systoleTime1, systoleTime2, systoleTime3 = None, None, None tstart = time.time() @@ -78,7 +78,7 @@ # T + 0 if oxi.peaks[-1] == 1: - beat = Sound('C', secs=0.1) + beat = Sound("C", secs=0.1) beat.play() systoleTime1 = time.time() systoleTime2 = time.time() @@ -86,32 +86,30 @@ # T + 1/4 if systoleTime1 is not None: - if time.time() - systoleTime1 >= ((oxi.instant_rr[-1]/4)/1000): - diastole1 = Sound('E', secs=0.1) + if time.time() - systoleTime1 >= ((oxi.instant_rr[-1] / 4) / 1000): + diastole1 = Sound("E", secs=0.1) diastole1.play() systoleTime1 = None # T + 2/4 if systoleTime2 is not None: - if time.time() - systoleTime2 >= ( - ((oxi.instant_rr[-1]/4) * 2)/1000): - diastole2 = Sound('G', secs=0.1) + if time.time() - systoleTime2 >= (((oxi.instant_rr[-1] / 4) * 2) / 1000): + diastole2 = Sound("G", secs=0.1) diastole2.play() systoleTime2 = None # T + 3/4 if systoleTime3 is not None: - if time.time() - systoleTime3 >= ( - ((oxi.instant_rr[-1]/4) * 3)/1000): - diastole3 = Sound('A', secs=0.1) + if time.time() - systoleTime3 >= (((oxi.instant_rr[-1] / 4) * 3) / 1000): + diastole3 = Sound("A", secs=0.1) diastole3.play() systoleTime3 = None # Track the note status - oxi.channels['Channel_0'][-1] = beat.status - oxi.channels['Channel_1'][-1] = diastole1.status - oxi.channels['Channel_2'][-1] = diastole2.status - oxi.channels['Channel_3'][-1] = diastole3.status + oxi.channels["Channel_0"][-1] = beat.status + oxi.channels["Channel_1"][-1] = diastole1.status + oxi.channels["Channel_2"][-1] = diastole2.status + oxi.channels["Channel_3"][-1] = diastole3.status #%% # Events @@ -129,11 +127,12 @@ angles = [] x = np.asarray(oxi.peaks) for ev in oxi.channels: - events = norm_triggers(np.asarray(oxi.channels[ev]), threshold=1, n=40, - direction='higher') + events = norm_triggers( + np.asarray(oxi.channels[ev]), threshold=1, n=40, direction="higher" + ) angles.append(to_angles(np.where(x)[0], np.where(events)[0])) -palette = itertools.cycle(sns.color_palette('deep')) +palette = itertools.cycle(sns.color_palette("deep")) ax = plt.subplot(111, polar=True) for i in angles: circular(i, color=next(palette), ax=ax) diff --git a/source/auto_examples/plot_HeartBeatEvokedArpeggios.py.md5 b/source/auto_examples/plot_HeartBeatEvokedArpeggios.py.md5 index e8f830d2..5e585e14 100644 --- a/source/auto_examples/plot_HeartBeatEvokedArpeggios.py.md5 +++ b/source/auto_examples/plot_HeartBeatEvokedArpeggios.py.md5 @@ -1 +1 @@ -029c9e4a34b72dc4b6002dd306d71f25 \ No newline at end of file +b3a99b467f1d4d705f9233756f6a06d8 \ No newline at end of file diff --git a/source/auto_examples/plot_HeartBeatEvokedArpeggios.rst b/source/auto_examples/plot_HeartBeatEvokedArpeggios.rst index b2eac1a0..f681d86d 100644 --- a/source/auto_examples/plot_HeartBeatEvokedArpeggios.rst +++ b/source/auto_examples/plot_HeartBeatEvokedArpeggios.rst @@ -1,12 +1,21 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples\plot_HeartBeatEvokedArpeggios.py" +.. LINE NUMBERS ARE GIVEN BELOW. + .. only:: html .. note:: :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code - .. rst-class:: sphx-glr-example-title + Click :ref:`here ` + to download the full example code - .. _sphx_glr_auto_examples_plot_HeartBeatEvokedArpeggios.py: +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_HeartBeatEvokedArpeggios.py: Heartbeat Evoked Arpeggios @@ -24,6 +33,7 @@ On this basis, each presentation time is calibrated based on the previous RR-int This procedure can easily be adapted to create a standard interoception task, e.g. by either presenting tones at no delay (systole, s+) or at a fixed offset (diastole, s-). +.. GENERATED FROM PYTHON SOURCE LINES 17-34 .. code-block:: default @@ -31,31 +41,89 @@ tones at no delay (systole, s+) or at a fixed offset (diastole, s-). # Author: Nicolas Legrand # Licence: GPL v3 + import itertools import time + + import matplotlib.pyplot as plt import numpy as np - import itertools import seaborn as sns - import matplotlib.pyplot as plt from psychopy.sound import Sound - from systole.utils import norm_triggers from systole import serialSim - from systole.utils import to_angles from systole.plotting import circular from systole.recording import Oximeter + from systole.utils import norm_triggers, to_angles + + + + + +.. rst-class:: sphx-glr-script-out + + Out: + + .. code-block:: none + + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1582: UserWarning: + + Trying to register the cmap 'rocket' which already exists. + + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1583: UserWarning: + + Trying to register the cmap 'rocket_r' which already exists. + + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1582: UserWarning: + + Trying to register the cmap 'mako' which already exists. + + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1583: UserWarning: + + Trying to register the cmap 'mako_r' which already exists. + + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1582: UserWarning: + + Trying to register the cmap 'icefire' which already exists. + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1583: UserWarning: + Trying to register the cmap 'icefire_r' which already exists. + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1582: UserWarning: + Trying to register the cmap 'vlag' which already exists. + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1583: UserWarning: + Trying to register the cmap 'vlag_r' which already exists. + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1582: UserWarning: + + Trying to register the cmap 'flare' which already exists. + + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1583: UserWarning: + + Trying to register the cmap 'flare_r' which already exists. + + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1582: UserWarning: + + Trying to register the cmap 'crest' which already exists. + + c:\programdata\anaconda3\lib\site-packages\seaborn\cm.py:1583: UserWarning: + + Trying to register the cmap 'crest_r' which already exists. + + + + + +.. GENERATED FROM PYTHON SOURCE LINES 35-39 Recording --------- For the purpose of demonstration, here we simulate data acquisition through the pulse oximeter using pre-recorded signal. +.. GENERATED FROM PYTHON SOURCE LINES 39-42 .. code-block:: default @@ -69,17 +137,24 @@ the pulse oximeter using pre-recorded signal. +.. GENERATED FROM PYTHON SOURCE LINES 43-46 + If you want to allow online data acquisition, you should uncomment the following lines and provide the reference of the COM port where the pulse oximeter is plugged in. +.. GENERATED FROM PYTHON SOURCE LINES 48-52 + .. code-block:: python import serial ser = serial.Serial('COM4') # Change this value according to your setup +.. GENERATED FROM PYTHON SOURCE LINES 54-55 + Create an Oximeter instance, initialize recording and record for 10 seconds +.. GENERATED FROM PYTHON SOURCE LINES 55-58 .. code-block:: default @@ -101,16 +176,19 @@ Create an Oximeter instance, initialize recording and record for 10 seconds +.. GENERATED FROM PYTHON SOURCE LINES 59-60 + Create an Oxymeter instance, initialize recording and record for 10 seconds +.. GENERATED FROM PYTHON SOURCE LINES 60-114 .. code-block:: default - beat = Sound('C', secs=0.1) - diastole1 = Sound('E', secs=0.1) - diastole2 = Sound('G', secs=0.1) - diastole3 = Sound('Bfl', secs=0.1) + beat = Sound("C", secs=0.1) + diastole1 = Sound("E", secs=0.1) + diastole2 = Sound("G", secs=0.1) + diastole3 = Sound("Bfl", secs=0.1) systoleTime1, systoleTime2, systoleTime3 = None, None, None tstart = time.time() @@ -127,7 +205,7 @@ Create an Oxymeter instance, initialize recording and record for 10 seconds # T + 0 if oxi.peaks[-1] == 1: - beat = Sound('C', secs=0.1) + beat = Sound("C", secs=0.1) beat.play() systoleTime1 = time.time() systoleTime2 = time.time() @@ -135,32 +213,30 @@ Create an Oxymeter instance, initialize recording and record for 10 seconds # T + 1/4 if systoleTime1 is not None: - if time.time() - systoleTime1 >= ((oxi.instant_rr[-1]/4)/1000): - diastole1 = Sound('E', secs=0.1) + if time.time() - systoleTime1 >= ((oxi.instant_rr[-1] / 4) / 1000): + diastole1 = Sound("E", secs=0.1) diastole1.play() systoleTime1 = None # T + 2/4 if systoleTime2 is not None: - if time.time() - systoleTime2 >= ( - ((oxi.instant_rr[-1]/4) * 2)/1000): - diastole2 = Sound('G', secs=0.1) + if time.time() - systoleTime2 >= (((oxi.instant_rr[-1] / 4) * 2) / 1000): + diastole2 = Sound("G", secs=0.1) diastole2.play() systoleTime2 = None # T + 3/4 if systoleTime3 is not None: - if time.time() - systoleTime3 >= ( - ((oxi.instant_rr[-1]/4) * 3)/1000): - diastole3 = Sound('A', secs=0.1) + if time.time() - systoleTime3 >= (((oxi.instant_rr[-1] / 4) * 3) / 1000): + diastole3 = Sound("A", secs=0.1) diastole3.play() systoleTime3 = None # Track the note status - oxi.channels['Channel_0'][-1] = beat.status - oxi.channels['Channel_1'][-1] = diastole1.status - oxi.channels['Channel_2'][-1] = diastole2.status - oxi.channels['Channel_3'][-1] = diastole3.status + oxi.channels["Channel_0"][-1] = beat.status + oxi.channels["Channel_1"][-1] = diastole1.status + oxi.channels["Channel_2"][-1] = diastole2.status + oxi.channels["Channel_3"][-1] = diastole3.status @@ -169,9 +245,12 @@ Create an Oxymeter instance, initialize recording and record for 10 seconds +.. GENERATED FROM PYTHON SOURCE LINES 115-117 + Events -------- +.. GENERATED FROM PYTHON SOURCE LINES 117-123 .. code-block:: default @@ -192,9 +271,12 @@ Events +.. GENERATED FROM PYTHON SOURCE LINES 124-126 + Cardiac cycle ------------- +.. GENERATED FROM PYTHON SOURCE LINES 126-139 .. code-block:: default @@ -202,11 +284,12 @@ Cardiac cycle angles = [] x = np.asarray(oxi.peaks) for ev in oxi.channels: - events = norm_triggers(np.asarray(oxi.channels[ev]), threshold=1, n=40, - direction='higher') + events = norm_triggers( + np.asarray(oxi.channels[ev]), threshold=1, n=40, direction="higher" + ) angles.append(to_angles(np.where(x)[0], np.where(events)[0])) - palette = itertools.cycle(sns.color_palette('deep')) + palette = itertools.cycle(sns.color_palette("deep")) ax = plt.subplot(111, polar=True) for i in angles: circular(i, color=next(palette), ax=ax) @@ -218,13 +301,24 @@ Cardiac cycle :class: sphx-glr-single-img +.. rst-class:: sphx-glr-script-out + + Out: + + .. code-block:: none + + c:\programdata\anaconda3\lib\site-packages\systole\plotting.py:697: UserWarning: + + FixedFormatter should only be used together with FixedLocator + + .. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 36.528 seconds) + **Total running time of the script:** ( 0 minutes 41.321 seconds) .. _sphx_glr_download_auto_examples_plot_HeartBeatEvokedArpeggios.py: diff --git a/source/auto_examples/plot_HeartBeatEvokedArpeggios_codeobj.pickle b/source/auto_examples/plot_HeartBeatEvokedArpeggios_codeobj.pickle index 5335154a..ea881b4e 100644 Binary files a/source/auto_examples/plot_HeartBeatEvokedArpeggios_codeobj.pickle and b/source/auto_examples/plot_HeartBeatEvokedArpeggios_codeobj.pickle differ diff --git a/source/auto_examples/plot_InstantaneousHeartRate.ipynb b/source/auto_examples/plot_InstantaneousHeartRate.ipynb index 419dadc4..c685875a 100644 --- a/source/auto_examples/plot_InstantaneousHeartRate.ipynb +++ b/source/auto_examples/plot_InstantaneousHeartRate.ipynb @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\nInstantaneous Heart Rate\n========================\n\nThis example show how to record PPG signals using the `Nonin 3012LP\nXpod USB pulse oximeter `_ and the `Nonin\n8000SM 'soft-clip' fingertip sensors `_.\nPeaks are automatically labelled online and the instantaneous heart rate is\nplotted.\n" + "\n# Instantaneous Heart Rate\n\nThis example show how to record PPG signals using the `Nonin 3012LP\nXpod USB pulse oximeter `_ and the `Nonin\n8000SM 'soft-clip' fingertip sensors `_.\nPeaks are automatically labelled online and the instantaneous heart rate is\nplotted.\n" ] }, { @@ -26,14 +26,14 @@ }, "outputs": [], "source": [ - "# Author: Nicolas Legrand \n# Licence: GPL v3\n\nfrom systole import serialSim\nfrom systole.utils import heart_rate\nfrom systole.recording import Oximeter\nimport matplotlib.pyplot as plt\nimport numpy as np" + "# Author: Nicolas Legrand \n# Licence: GPL v3\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nfrom systole import serialSim\nfrom systole.recording import Oximeter\nfrom systole.utils import heart_rate" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Recording\n---------\nFor the demonstration purpose, here we simulate data acquisition through\nthe pulse oximeter using pre-recorded signal.\n\n" + "## Recording\nFor the demonstration purpose, here we simulate data acquisition through\nthe pulse oximeter using pre-recorded signal.\n\n" ] }, { @@ -76,7 +76,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Plotting\n--------\n\n" + "## Plotting\n\n" ] }, { @@ -87,7 +87,7 @@ }, "outputs": [], "source": [ - "fig, ax = plt.subplots(3, 1, figsize=(13, 8), sharex=True)\noxi.plot_recording(ax=ax[0])\n\nax[1].plot(oxi.times, oxi.peaks, 'k')\nax[1].set_title('Peaks vector', fontweight='bold')\nax[1].set_xlabel('Time (s)')\nax[1].set_ylabel('Peak\\n detection')\n\n\nhr, time = heart_rate(oxi.peaks, sfreq=75, unit='rr', kind='cubic')\nax[2].plot(time, hr, label='Interpolated HR', linestyle='--', color='gray')\nax[2].plot(np.array(oxi.times)[np.where(oxi.peaks)[0]],\n hr[np.where(oxi.peaks)[0]], 'ro', label='Instantaneous HR')\nax[2].set_xlabel('Time (s)')\nax[2].set_title('Instantaneous Heart Rate', fontweight='bold')\nax[2].set_ylabel('RR intervals (ms)')\n\nplt.tight_layout()" + "fig, ax = plt.subplots(3, 1, figsize=(13, 8), sharex=True)\noxi.plot_recording(ax=ax[0])\n\nax[1].plot(oxi.times, oxi.peaks, \"k\")\nax[1].set_title(\"Peaks vector\", fontweight=\"bold\")\nax[1].set_xlabel(\"Time (s)\")\nax[1].set_ylabel(\"Peak\\n detection\")\n\n\nhr, time = heart_rate(oxi.peaks, sfreq=75, unit=\"rr\", kind=\"cubic\")\nax[2].plot(time, hr, label=\"Interpolated HR\", linestyle=\"--\", color=\"gray\")\nax[2].plot(\n np.array(oxi.times)[np.where(oxi.peaks)[0]],\n hr[np.where(oxi.peaks)[0]],\n \"ro\",\n label=\"Instantaneous HR\",\n)\nax[2].set_xlabel(\"Time (s)\")\nax[2].set_title(\"Instantaneous Heart Rate\", fontweight=\"bold\")\nax[2].set_ylabel(\"RR intervals (ms)\")\n\nplt.tight_layout()" ] } ], @@ -107,7 +107,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.7.9" } }, "nbformat": 4, diff --git a/source/auto_examples/plot_InstantaneousHeartRate.py b/source/auto_examples/plot_InstantaneousHeartRate.py index 664ed6a0..985e0c81 100644 --- a/source/auto_examples/plot_InstantaneousHeartRate.py +++ b/source/auto_examples/plot_InstantaneousHeartRate.py @@ -12,12 +12,13 @@ # Author: Nicolas Legrand # Licence: GPL v3 -from systole import serialSim -from systole.utils import heart_rate -from systole.recording import Oximeter import matplotlib.pyplot as plt import numpy as np +from systole import serialSim +from systole.recording import Oximeter +from systole.utils import heart_rate + #%% # Recording # --------- @@ -47,18 +48,22 @@ fig, ax = plt.subplots(3, 1, figsize=(13, 8), sharex=True) oxi.plot_recording(ax=ax[0]) -ax[1].plot(oxi.times, oxi.peaks, 'k') -ax[1].set_title('Peaks vector', fontweight='bold') -ax[1].set_xlabel('Time (s)') -ax[1].set_ylabel('Peak\n detection') +ax[1].plot(oxi.times, oxi.peaks, "k") +ax[1].set_title("Peaks vector", fontweight="bold") +ax[1].set_xlabel("Time (s)") +ax[1].set_ylabel("Peak\n detection") -hr, time = heart_rate(oxi.peaks, sfreq=75, unit='rr', kind='cubic') -ax[2].plot(time, hr, label='Interpolated HR', linestyle='--', color='gray') -ax[2].plot(np.array(oxi.times)[np.where(oxi.peaks)[0]], - hr[np.where(oxi.peaks)[0]], 'ro', label='Instantaneous HR') -ax[2].set_xlabel('Time (s)') -ax[2].set_title('Instantaneous Heart Rate', fontweight='bold') -ax[2].set_ylabel('RR intervals (ms)') +hr, time = heart_rate(oxi.peaks, sfreq=75, unit="rr", kind="cubic") +ax[2].plot(time, hr, label="Interpolated HR", linestyle="--", color="gray") +ax[2].plot( + np.array(oxi.times)[np.where(oxi.peaks)[0]], + hr[np.where(oxi.peaks)[0]], + "ro", + label="Instantaneous HR", +) +ax[2].set_xlabel("Time (s)") +ax[2].set_title("Instantaneous Heart Rate", fontweight="bold") +ax[2].set_ylabel("RR intervals (ms)") plt.tight_layout() diff --git a/source/auto_examples/plot_InstantaneousHeartRate.py.md5 b/source/auto_examples/plot_InstantaneousHeartRate.py.md5 index 8dd58a8b..ffacad5c 100644 --- a/source/auto_examples/plot_InstantaneousHeartRate.py.md5 +++ b/source/auto_examples/plot_InstantaneousHeartRate.py.md5 @@ -1 +1 @@ -2e5f5defbd94632a635de74bc150aaa1 \ No newline at end of file +bab8f131b47340be7647609e2fbff9e0 \ No newline at end of file diff --git a/source/auto_examples/plot_InstantaneousHeartRate.rst b/source/auto_examples/plot_InstantaneousHeartRate.rst index 515d3f07..d83201d9 100644 --- a/source/auto_examples/plot_InstantaneousHeartRate.rst +++ b/source/auto_examples/plot_InstantaneousHeartRate.rst @@ -1,7 +1,18 @@ -.. note:: - :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples\plot_InstantaneousHeartRate.py" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + Click :ref:`here ` + to download the full example code + .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_plot_InstantaneousHeartRate.py: @@ -16,6 +27,7 @@ Xpod USB pulse oximeter `_ and the `Nonin Peaks are automatically labelled online and the instantaneous heart rate is plotted. +.. GENERATED FROM PYTHON SOURCE LINES 11-22 .. code-block:: default @@ -23,12 +35,13 @@ plotted. # Author: Nicolas Legrand # Licence: GPL v3 - from systole import serialSim - from systole.utils import heart_rate - from systole.recording import Oximeter import matplotlib.pyplot as plt import numpy as np + from systole import serialSim + from systole.recording import Oximeter + from systole.utils import heart_rate + @@ -36,11 +49,14 @@ plotted. +.. GENERATED FROM PYTHON SOURCE LINES 23-27 + Recording --------- For the demonstration purpose, here we simulate data acquisition through the pulse oximeter using pre-recorded signal. +.. GENERATED FROM PYTHON SOURCE LINES 27-30 .. code-block:: default @@ -54,15 +70,20 @@ the pulse oximeter using pre-recorded signal. +.. GENERATED FROM PYTHON SOURCE LINES 31-34 + If you want to enable online data acquisition, you should uncomment the following lines and provide the reference of the COM port where the pulse oximeter is plugged in. +.. GENERATED FROM PYTHON SOURCE LINES 36-40 + .. code-block:: python import serial ser = serial.Serial('COM4') # Change this value according to your setup +.. GENERATED FROM PYTHON SOURCE LINES 40-45 .. code-block:: default @@ -83,38 +104,46 @@ oximeter is plugged in. Reset input buffer - + + +.. GENERATED FROM PYTHON SOURCE LINES 46-48 Plotting -------- +.. GENERATED FROM PYTHON SOURCE LINES 48-70 .. code-block:: default fig, ax = plt.subplots(3, 1, figsize=(13, 8), sharex=True) oxi.plot_recording(ax=ax[0]) - ax[1].plot(oxi.times, oxi.peaks, 'k') - ax[1].set_title('Peaks vector', fontweight='bold') - ax[1].set_xlabel('Time (s)') - ax[1].set_ylabel('Peak\n detection') + ax[1].plot(oxi.times, oxi.peaks, "k") + ax[1].set_title("Peaks vector", fontweight="bold") + ax[1].set_xlabel("Time (s)") + ax[1].set_ylabel("Peak\n detection") - hr, time = heart_rate(oxi.peaks, sfreq=75, unit='rr', kind='cubic') - ax[2].plot(time, hr, label='Interpolated HR', linestyle='--', color='gray') - ax[2].plot(np.array(oxi.times)[np.where(oxi.peaks)[0]], - hr[np.where(oxi.peaks)[0]], 'ro', label='Instantaneous HR') - ax[2].set_xlabel('Time (s)') - ax[2].set_title('Instantaneous Heart Rate', fontweight='bold') - ax[2].set_ylabel('RR intervals (ms)') + hr, time = heart_rate(oxi.peaks, sfreq=75, unit="rr", kind="cubic") + ax[2].plot(time, hr, label="Interpolated HR", linestyle="--", color="gray") + ax[2].plot( + np.array(oxi.times)[np.where(oxi.peaks)[0]], + hr[np.where(oxi.peaks)[0]], + "ro", + label="Instantaneous HR", + ) + ax[2].set_xlabel("Time (s)") + ax[2].set_title("Instantaneous Heart Rate", fontweight="bold") + ax[2].set_ylabel("RR intervals (ms)") plt.tight_layout() .. image:: /auto_examples/images/sphx_glr_plot_InstantaneousHeartRate_001.png + :alt: Oximeter recording, Peaks vector, Instantaneous Heart Rate :class: sphx-glr-single-img @@ -124,7 +153,7 @@ Plotting .. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 31.257 seconds) + **Total running time of the script:** ( 0 minutes 32.270 seconds) .. _sphx_glr_download_auto_examples_plot_InstantaneousHeartRate.py: @@ -137,13 +166,13 @@ Plotting - .. container:: sphx-glr-download + .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_InstantaneousHeartRate.py ` - .. container:: sphx-glr-download + .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_InstantaneousHeartRate.ipynb ` diff --git a/source/auto_examples/plot_InstantaneousHeartRate_codeobj.pickle b/source/auto_examples/plot_InstantaneousHeartRate_codeobj.pickle index 9f809667..7b461f2f 100644 Binary files a/source/auto_examples/plot_InstantaneousHeartRate_codeobj.pickle and b/source/auto_examples/plot_InstantaneousHeartRate_codeobj.pickle differ diff --git a/source/auto_examples/plot_InteractiveVisualizations.ipynb b/source/auto_examples/plot_InteractiveVisualizations.ipynb index 4d50f0b9..720e98e7 100644 --- a/source/auto_examples/plot_InteractiveVisualizations.ipynb +++ b/source/auto_examples/plot_InteractiveVisualizations.ipynb @@ -15,7 +15,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\nInteractive visualization\n=========================\n\nThe pipeline of physiological recording can often benefiit from\ninteractive data visualization and exploration to guide data analysis. Systole\ninclude functions build on the top of Plotly (https://plotly.com/) for\ninteractive data visualization and dashboard integration\n(https://plotly.com/dash/).\n" + "\n# Interactive visualization\n\nThe pipeline of physiological recording can often benefiit from\ninteractive data visualization and exploration to guide data analysis. Systole\ninclude functions build on the top of Plotly (https://plotly.com/) for\ninteractive data visualization and dashboard integration\n(https://plotly.com/dash/).\n" ] }, { @@ -37,14 +37,14 @@ }, "outputs": [], "source": [ - "import plotly\nfrom systole.detection import rr_artefacts\nfrom systole import import_rr, import_ppg\nfrom systole.plotly import plot_subspaces, plot_frequency, plot_raw,\\\n plot_timedomain, plot_nonlinear" + "import plotly\n\nfrom systole import import_ppg, import_rr\nfrom systole.detection import rr_artefacts\nfrom systole.plotly import (\n plot_frequency,\n plot_nonlinear,\n plot_raw,\n plot_subspaces,\n plot_timedomain,\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Raw data\n--------\n\n\n" + "## Raw data\n\n\n" ] }, { @@ -55,14 +55,14 @@ }, "outputs": [], "source": [ - "ppg = import_ppg()[0]\nplot_raw(ppg)" + "ppg = import_ppg()\nplot_raw(ppg)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "HRV analyses\n------------\n\n" + "## HRV analyses\n\n" ] }, { @@ -80,7 +80,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Frequency domain\n----------------\n\n" + "## Frequency domain\n\n" ] }, { @@ -98,7 +98,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Frequency domain\n----------------\n\n" + "## Frequency domain\n\n" ] }, { @@ -116,7 +116,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Nonlinear domain\n----------------\n\n" + "## Nonlinear domain\n\n" ] }, { @@ -134,7 +134,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Artefact detection\n------------------\n\n" + "## Artefact detection\n\n" ] }, { @@ -152,7 +152,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Subspaces visualization\n-----------------------\nYou can visualize the two main subspaces and spot outliers. The left pamel\nplot subspaces that are more sensitive to ectopic beats detection. The right\npanel plot subspaces that will be more sensitive to long or short beats,\ncomprizing the extra and missed beats.\n\n" + "## Subspaces visualization\nYou can visualize the two main subspaces and spot outliers. The left pamel\nplot subspaces that are more sensitive to ectopic beats detection. The right\npanel plot subspaces that will be more sensitive to long or short beats,\ncomprizing the extra and missed beats.\n\n" ] }, { @@ -183,7 +183,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.6" + "version": "3.7.9" } }, "nbformat": 4, diff --git a/source/auto_examples/plot_InteractiveVisualizations.py b/source/auto_examples/plot_InteractiveVisualizations.py index b16cb2ce..7d82a967 100644 --- a/source/auto_examples/plot_InteractiveVisualizations.py +++ b/source/auto_examples/plot_InteractiveVisualizations.py @@ -14,16 +14,22 @@ #%% import plotly + +from systole import import_ppg, import_rr from systole.detection import rr_artefacts -from systole import import_rr, import_ppg -from systole.plotly import plot_subspaces, plot_frequency, plot_raw,\ - plot_timedomain, plot_nonlinear +from systole.plotly import ( + plot_frequency, + plot_nonlinear, + plot_raw, + plot_subspaces, + plot_timedomain, +) #%% # Raw data # -------- # -ppg = import_ppg()[0] +ppg = import_ppg() plot_raw(ppg) #%% diff --git a/source/auto_examples/plot_InteractiveVisualizations.py.md5 b/source/auto_examples/plot_InteractiveVisualizations.py.md5 index 823aa025..88d2919e 100644 --- a/source/auto_examples/plot_InteractiveVisualizations.py.md5 +++ b/source/auto_examples/plot_InteractiveVisualizations.py.md5 @@ -1 +1 @@ -cfae274584268d9e92e0554406ece01f \ No newline at end of file +de495e58aace86865c15275ef84c8cc7 \ No newline at end of file diff --git a/source/auto_examples/plot_InteractiveVisualizations.rst b/source/auto_examples/plot_InteractiveVisualizations.rst index 760572dd..3e355857 100644 --- a/source/auto_examples/plot_InteractiveVisualizations.rst +++ b/source/auto_examples/plot_InteractiveVisualizations.rst @@ -1,12 +1,21 @@ + +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "auto_examples\plot_InteractiveVisualizations.py" +.. LINE NUMBERS ARE GIVEN BELOW. + .. only:: html .. note:: :class: sphx-glr-download-link-note - Click :ref:`here ` to download the full example code - .. rst-class:: sphx-glr-example-title + Click :ref:`here ` + to download the full example code - .. _sphx_glr_auto_examples_plot_InteractiveVisualizations.py: +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_auto_examples_plot_InteractiveVisualizations.py: Interactive visualization @@ -18,6 +27,7 @@ include functions build on the top of Plotly (https://plotly.com/) for interactive data visualization and dashboard integration (https://plotly.com/dash/). +.. GENERATED FROM PYTHON SOURCE LINES 11-15 .. code-block:: default @@ -32,14 +42,21 @@ interactive data visualization and dashboard integration +.. GENERATED FROM PYTHON SOURCE LINES 16-28 .. code-block:: default import plotly + + from systole import import_ppg, import_rr from systole.detection import rr_artefacts - from systole import import_rr, import_ppg - from systole.plotly import plot_subspaces, plot_frequency, plot_raw,\ - plot_timedomain, plot_nonlinear + from systole.plotly import ( + plot_frequency, + plot_nonlinear, + plot_raw, + plot_subspaces, + plot_timedomain, + ) @@ -48,14 +65,17 @@ interactive data visualization and dashboard integration +.. GENERATED FROM PYTHON SOURCE LINES 29-32 + Raw data -------- +.. GENERATED FROM PYTHON SOURCE LINES 32-35 .. code-block:: default - ppg = import_ppg()[0] + ppg = import_ppg() plot_raw(ppg) @@ -63,36 +83,21 @@ Raw data -.. only:: builder_html +.. raw:: html - .. raw:: html +
+
+
+
+
+
-
- - - -
- -
-
-
+.. GENERATED FROM PYTHON SOURCE LINES 36-38 HRV analyses ------------ +.. GENERATED FROM PYTHON SOURCE LINES 38-41 .. code-block:: default @@ -106,9 +111,12 @@ HRV analyses +.. GENERATED FROM PYTHON SOURCE LINES 42-44 + Frequency domain ---------------- +.. GENERATED FROM PYTHON SOURCE LINES 44-47 .. code-block:: default @@ -125,9 +133,12 @@ Frequency domain +.. GENERATED FROM PYTHON SOURCE LINES 48-50 + Frequency domain ---------------- +.. GENERATED FROM PYTHON SOURCE LINES 50-53 .. code-block:: default @@ -144,9 +155,12 @@ Frequency domain +.. GENERATED FROM PYTHON SOURCE LINES 54-56 + Nonlinear domain ---------------- +.. GENERATED FROM PYTHON SOURCE LINES 56-59 .. code-block:: default @@ -163,9 +177,12 @@ Nonlinear domain +.. GENERATED FROM PYTHON SOURCE LINES 60-62 + Artefact detection ------------------ +.. GENERATED FROM PYTHON SOURCE LINES 62-65 .. code-block:: default @@ -179,6 +196,8 @@ Artefact detection +.. GENERATED FROM PYTHON SOURCE LINES 66-72 + Subspaces visualization ----------------------- You can visualize the two main subspaces and spot outliers. The left pamel @@ -186,6 +205,7 @@ plot subspaces that are more sensitive to ectopic beats detection. The right panel plot subspaces that will be more sensitive to long or short beats, comprizing the extra and missed beats. +.. GENERATED FROM PYTHON SOURCE LINES 72-74 .. code-block:: default @@ -196,37 +216,19 @@ comprizing the extra and missed beats. -.. only:: builder_html - - .. raw:: html +.. raw:: html -
- - - -
- -
-
-
+
+
+
+
+
+
.. rst-class:: sphx-glr-timing - **Total running time of the script:** ( 0 minutes 19.102 seconds) + **Total running time of the script:** ( 0 minutes 42.377 seconds) .. _sphx_glr_download_auto_examples_plot_InteractiveVisualizations.py: diff --git a/source/auto_examples/plot_InteractiveVisualizations_codeobj.pickle b/source/auto_examples/plot_InteractiveVisualizations_codeobj.pickle index 7c843e71..c6eeca89 100644 Binary files a/source/auto_examples/plot_InteractiveVisualizations_codeobj.pickle and b/source/auto_examples/plot_InteractiveVisualizations_codeobj.pickle differ diff --git a/source/auto_examples/sg_execution_times.rst b/source/auto_examples/sg_execution_times.rst index 7828e41a..7d1bec05 100644 --- a/source/auto_examples/sg_execution_times.rst +++ b/source/auto_examples/sg_execution_times.rst @@ -5,10 +5,14 @@ Computation times ================= -**00:00.493** total execution time for **auto_examples** files: +**00:42.377** total execution time for **auto_examples** files: +---------------------------------------------------------------------------------------------------------+-----------+--------+ -| :ref:`sphx_glr_auto_examples_plot_ArtefactsCorrection.py` (``plot_ArtefactsCorrection.py``) | 00:00.493 | 0.0 MB | +| :ref:`sphx_glr_auto_examples_plot_InteractiveVisualizations.py` (``plot_InteractiveVisualizations.py``) | 00:42.377 | 0.0 MB | ++---------------------------------------------------------------------------------------------------------+-----------+--------+ +| :ref:`sphx_glr_auto_examples_Tutorial_recording.py` (``Tutorial_recording.py``) | 00:00.000 | 0.0 MB | ++---------------------------------------------------------------------------------------------------------+-----------+--------+ +| :ref:`sphx_glr_auto_examples_plot_ArtefactsCorrection.py` (``plot_ArtefactsCorrection.py``) | 00:00.000 | 0.0 MB | +---------------------------------------------------------------------------------------------------------+-----------+--------+ | :ref:`sphx_glr_auto_examples_plot_ArtefactsDetection.py` (``plot_ArtefactsDetection.py``) | 00:00.000 | 0.0 MB | +---------------------------------------------------------------------------------------------------------+-----------+--------+ @@ -16,5 +20,3 @@ Computation times +---------------------------------------------------------------------------------------------------------+-----------+--------+ | :ref:`sphx_glr_auto_examples_plot_InstantaneousHeartRate.py` (``plot_InstantaneousHeartRate.py``) | 00:00.000 | 0.0 MB | +---------------------------------------------------------------------------------------------------------+-----------+--------+ -| :ref:`sphx_glr_auto_examples_plot_InteractiveVisualizations.py` (``plot_InteractiveVisualizations.py``) | 00:00.000 | 0.0 MB | -+---------------------------------------------------------------------------------------------------------+-----------+--------+ diff --git a/source/changelog.rst b/source/changelog.rst index 877aeb2d..a4ea593a 100644 --- a/source/changelog.rst +++ b/source/changelog.rst @@ -6,8 +6,23 @@ What's new .. contents:: Table of Contents :depth: 2 +v0.1.3 (April 2021) +------------------- + +**Enhancements** +a. :py:func:`systole.plotly.plot_raw()`: add `ecg_method` parameter to control the ECG peak detection method used. +b. Download dataset directly from GitHub instead of copying the files at install. +c. Haromonisation of :py:func:`systole.plotting.plot_raw()` and :py:func:`systole.plotting.plot_raw()` (replace the `plot_hr()` function), and :py:func:`systole.plotly.plot_subspaces()` and :py:func:`systole.plotly.plot_subspaces()`. +d. The :py:class:`systole.recording.Oximeter()` class has been improved: + * :py:func:`systole.recording.Oximeter.setup()` has an `nAttempts` argument so it will not run forever if no valid signal is recordedfor a given number of attempts (default is 100). + * :py:func:`systole.recording.Oximeter.check()` has been updated and accept data format #7 from Xpods, allowing more flexibility. + * :py:func:`systole.recording.Oximeter.save()` will now save additional channels and support `.txt` and `.npy` file extensions. + * Create a :py:func:`systole.recording.Oximeter.reset()` method to avoid improper use of `__init__()`. +e. Add pre-commit hooks, flake8, black and isort CI tests. +f. Add type hints and CI testing with mypy. + v0.1.2 (September 2020) --------------------- +----------------------- **New functions** diff --git a/source/conf.py b/source/conf.py index 0e66c8bb..8291a240 100644 --- a/source/conf.py +++ b/source/conf.py @@ -10,23 +10,27 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # +import time import sphinx_bootstrap_theme from plotly.io._sg_scraper import plotly_sg_scraper # -- Project information ----------------------------------------------------- -project = 'systole' -copyright = '2020, Nicolas Legrand' -author = 'Nicolas Legrand' -release = '0.1.1' +project = "systole" +copyright = u'2020-{}, Nicolas Legrand'.format(time.strftime("%Y")) +author = "Nicolas Legrand" +release = "0.1.3" -image_scrapers = ('matplotlib', plotly_sg_scraper,) +image_scrapers = ( + "matplotlib", + plotly_sg_scraper, +) sphinx_gallery_conf = { - 'examples_dirs': '../examples', - 'backreferences_dir': 'api', - 'image_scrapers': image_scrapers, + "examples_dirs": "../examples", + "backreferences_dir": "api", + "image_scrapers": image_scrapers, } # -- General configuration --------------------------------------------------- @@ -35,16 +39,18 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.mathjax', - 'sphinx.ext.doctest', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'sphinx.ext.autosummary', - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx_gallery.gen_gallery', - 'matplotlib.sphinxext.plot_directive', - 'numpydoc'] + "sphinx.ext.mathjax", + "sphinx.ext.doctest", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.autosummary", + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx_gallery.gen_gallery", + "matplotlib.sphinxext.plot_directive", + "numpydoc", + "jupyter_sphinx", +] # Generate the API documentation when building autosummary_generate = True @@ -57,13 +63,13 @@ plot_html_show_source_link = False # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -75,32 +81,34 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'bootstrap' +html_theme = "bootstrap" html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() html_theme_options = { - 'bootswatch_theme': "flatly", - 'bootstrap_version': "3", - 'navbar_class': "navbar", - 'navbar_links': [ - ("Functions", "api"), - ("Tutorials", "auto_examples/index"), - ("GitHub", "https://github.com/embodied-computation-group/systole", True), - ("What's new", "changelog") - ]} + "bootswatch_theme": "flatly", + "bootstrap_version": "3", + "navbar_class": "navbar", + "navbar_links": [ + ("Functions", "api"), + ("Tutorials", "auto_examples/index"), + ("GitHub", "https://github.com/embodied-computation-group/systole", True), + ("What's new", "changelog"), + ], +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +# html_static_path = ['_static'] -html_logo = 'images/logo.png' +html_logo = "images/logo.png" # -- Intersphinx ------------------------------------------------ intersphinx_mapping = { - 'numpy': ('http://docs.scipy.org/doc/numpy/', None), - 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), - 'matplotlib': ('http://matplotlib.org/', None), - 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), - 'seaborn': ('https://seaborn.pydata.org/', None), - 'sklearn': ('http://scikit-learn.org/stable', None)} + "numpy": ("http://docs.scipy.org/doc/numpy/", None), + "scipy": ("http://docs.scipy.org/doc/scipy/reference/", None), + "matplotlib": ("http://matplotlib.org/", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "seaborn": ("https://seaborn.pydata.org/", None), + "sklearn": ("http://scikit-learn.org/stable", None), +} diff --git a/source/generated/correction/systole.correction.correct_extra.rst b/source/generated/correction/systole.correction.correct_extra.rst new file mode 100644 index 00000000..ad949e5a --- /dev/null +++ b/source/generated/correction/systole.correction.correct_extra.rst @@ -0,0 +1,6 @@ +systole.correction.correct\_extra +================================= + +.. currentmodule:: systole.correction + +.. autofunction:: correct_extra \ No newline at end of file diff --git a/source/generated/correction/systole.correction.correct_extra_peaks.rst b/source/generated/correction/systole.correction.correct_extra_peaks.rst new file mode 100644 index 00000000..36c3c4d8 --- /dev/null +++ b/source/generated/correction/systole.correction.correct_extra_peaks.rst @@ -0,0 +1,6 @@ +systole.correction.correct\_extra\_peaks +======================================== + +.. currentmodule:: systole.correction + +.. autofunction:: correct_extra_peaks \ No newline at end of file diff --git a/source/generated/correction/systole.correction.correct_missed.rst b/source/generated/correction/systole.correction.correct_missed.rst new file mode 100644 index 00000000..975256c5 --- /dev/null +++ b/source/generated/correction/systole.correction.correct_missed.rst @@ -0,0 +1,6 @@ +systole.correction.correct\_missed +================================== + +.. currentmodule:: systole.correction + +.. autofunction:: correct_missed \ No newline at end of file diff --git a/source/generated/correction/systole.correction.correct_missed_peaks.rst b/source/generated/correction/systole.correction.correct_missed_peaks.rst new file mode 100644 index 00000000..b4c8ed99 --- /dev/null +++ b/source/generated/correction/systole.correction.correct_missed_peaks.rst @@ -0,0 +1,6 @@ +systole.correction.correct\_missed\_peaks +========================================= + +.. currentmodule:: systole.correction + +.. autofunction:: correct_missed_peaks \ No newline at end of file diff --git a/source/generated/correction/systole.correction.correct_peaks.rst b/source/generated/correction/systole.correction.correct_peaks.rst new file mode 100644 index 00000000..ca9f9e95 --- /dev/null +++ b/source/generated/correction/systole.correction.correct_peaks.rst @@ -0,0 +1,6 @@ +systole.correction.correct\_peaks +================================= + +.. currentmodule:: systole.correction + +.. autofunction:: correct_peaks \ No newline at end of file diff --git a/source/generated/correction/systole.correction.correct_rr.rst b/source/generated/correction/systole.correction.correct_rr.rst new file mode 100644 index 00000000..7a6dc763 --- /dev/null +++ b/source/generated/correction/systole.correction.correct_rr.rst @@ -0,0 +1,6 @@ +systole.correction.correct\_rr +============================== + +.. currentmodule:: systole.correction + +.. autofunction:: correct_rr \ No newline at end of file diff --git a/source/generated/correction/systole.correction.interpolate_bads.rst b/source/generated/correction/systole.correction.interpolate_bads.rst new file mode 100644 index 00000000..23093d28 --- /dev/null +++ b/source/generated/correction/systole.correction.interpolate_bads.rst @@ -0,0 +1,6 @@ +systole.correction.interpolate\_bads +==================================== + +.. currentmodule:: systole.correction + +.. autofunction:: interpolate_bads \ No newline at end of file diff --git a/source/generated/detection/systole.detection.ecg_peaks.rst b/source/generated/detection/systole.detection.ecg_peaks.rst new file mode 100644 index 00000000..ae88df32 --- /dev/null +++ b/source/generated/detection/systole.detection.ecg_peaks.rst @@ -0,0 +1,6 @@ +systole.detection.ecg\_peaks +============================ + +.. currentmodule:: systole.detection + +.. autofunction:: ecg_peaks \ No newline at end of file diff --git a/source/generated/detection/systole.detection.interpolate_clipping.rst b/source/generated/detection/systole.detection.interpolate_clipping.rst new file mode 100644 index 00000000..141efd2d --- /dev/null +++ b/source/generated/detection/systole.detection.interpolate_clipping.rst @@ -0,0 +1,6 @@ +systole.detection.interpolate\_clipping +======================================= + +.. currentmodule:: systole.detection + +.. autofunction:: interpolate_clipping \ No newline at end of file diff --git a/source/generated/detection/systole.detection.oxi_peaks.rst b/source/generated/detection/systole.detection.oxi_peaks.rst new file mode 100644 index 00000000..894cb58f --- /dev/null +++ b/source/generated/detection/systole.detection.oxi_peaks.rst @@ -0,0 +1,6 @@ +systole.detection.oxi\_peaks +============================ + +.. currentmodule:: systole.detection + +.. autofunction:: oxi_peaks \ No newline at end of file diff --git a/source/generated/detection/systole.detection.rr_artefacts.rst b/source/generated/detection/systole.detection.rr_artefacts.rst new file mode 100644 index 00000000..dd865b44 --- /dev/null +++ b/source/generated/detection/systole.detection.rr_artefacts.rst @@ -0,0 +1,6 @@ +systole.detection.rr\_artefacts +=============================== + +.. currentmodule:: systole.detection + +.. autofunction:: rr_artefacts \ No newline at end of file diff --git a/source/generated/hrv/systole.hrv.frequency_domain.rst b/source/generated/hrv/systole.hrv.frequency_domain.rst new file mode 100644 index 00000000..7c0ea782 --- /dev/null +++ b/source/generated/hrv/systole.hrv.frequency_domain.rst @@ -0,0 +1,6 @@ +systole.hrv.frequency\_domain +============================= + +.. currentmodule:: systole.hrv + +.. autofunction:: frequency_domain \ No newline at end of file diff --git a/source/generated/hrv/systole.hrv.nnX.rst b/source/generated/hrv/systole.hrv.nnX.rst new file mode 100644 index 00000000..7d0c300d --- /dev/null +++ b/source/generated/hrv/systole.hrv.nnX.rst @@ -0,0 +1,6 @@ +systole.hrv.nnX +=============== + +.. currentmodule:: systole.hrv + +.. autofunction:: nnX \ No newline at end of file diff --git a/source/generated/hrv/systole.hrv.nonlinear.rst b/source/generated/hrv/systole.hrv.nonlinear.rst new file mode 100644 index 00000000..7135f441 --- /dev/null +++ b/source/generated/hrv/systole.hrv.nonlinear.rst @@ -0,0 +1,6 @@ +systole.hrv.nonlinear +===================== + +.. currentmodule:: systole.hrv + +.. autofunction:: nonlinear \ No newline at end of file diff --git a/source/generated/hrv/systole.hrv.pnnX.rst b/source/generated/hrv/systole.hrv.pnnX.rst new file mode 100644 index 00000000..756fc422 --- /dev/null +++ b/source/generated/hrv/systole.hrv.pnnX.rst @@ -0,0 +1,6 @@ +systole.hrv.pnnX +================ + +.. currentmodule:: systole.hrv + +.. autofunction:: pnnX \ No newline at end of file diff --git a/source/generated/hrv/systole.hrv.rmssd.rst b/source/generated/hrv/systole.hrv.rmssd.rst new file mode 100644 index 00000000..65704bb1 --- /dev/null +++ b/source/generated/hrv/systole.hrv.rmssd.rst @@ -0,0 +1,6 @@ +systole.hrv.rmssd +================= + +.. currentmodule:: systole.hrv + +.. autofunction:: rmssd \ No newline at end of file diff --git a/source/generated/hrv/systole.hrv.time_domain.rst b/source/generated/hrv/systole.hrv.time_domain.rst new file mode 100644 index 00000000..58129ee4 --- /dev/null +++ b/source/generated/hrv/systole.hrv.time_domain.rst @@ -0,0 +1,6 @@ +systole.hrv.time\_domain +======================== + +.. currentmodule:: systole.hrv + +.. autofunction:: time_domain \ No newline at end of file diff --git a/source/generated/plotly/systole.plotly.plot_ectopic.rst b/source/generated/plotly/systole.plotly.plot_ectopic.rst new file mode 100644 index 00000000..b89aff11 --- /dev/null +++ b/source/generated/plotly/systole.plotly.plot_ectopic.rst @@ -0,0 +1,6 @@ +systole.plotly.plot\_ectopic +============================ + +.. currentmodule:: systole.plotly + +.. autofunction:: plot_ectopic \ No newline at end of file diff --git a/source/generated/plotly/systole.plotly.plot_frequency.rst b/source/generated/plotly/systole.plotly.plot_frequency.rst new file mode 100644 index 00000000..d62afc9d --- /dev/null +++ b/source/generated/plotly/systole.plotly.plot_frequency.rst @@ -0,0 +1,6 @@ +systole.plotly.plot\_frequency +============================== + +.. currentmodule:: systole.plotly + +.. autofunction:: plot_frequency \ No newline at end of file diff --git a/source/generated/plotly/systole.plotly.plot_nonlinear.rst b/source/generated/plotly/systole.plotly.plot_nonlinear.rst new file mode 100644 index 00000000..1981378b --- /dev/null +++ b/source/generated/plotly/systole.plotly.plot_nonlinear.rst @@ -0,0 +1,6 @@ +systole.plotly.plot\_nonlinear +============================== + +.. currentmodule:: systole.plotly + +.. autofunction:: plot_nonlinear \ No newline at end of file diff --git a/source/generated/plotly/systole.plotly.plot_raw.rst b/source/generated/plotly/systole.plotly.plot_raw.rst new file mode 100644 index 00000000..d27e955b --- /dev/null +++ b/source/generated/plotly/systole.plotly.plot_raw.rst @@ -0,0 +1,6 @@ +systole.plotly.plot\_raw +======================== + +.. currentmodule:: systole.plotly + +.. autofunction:: plot_raw \ No newline at end of file diff --git a/source/generated/plotly/systole.plotly.plot_shortLong.rst b/source/generated/plotly/systole.plotly.plot_shortLong.rst new file mode 100644 index 00000000..a6e684a3 --- /dev/null +++ b/source/generated/plotly/systole.plotly.plot_shortLong.rst @@ -0,0 +1,6 @@ +systole.plotly.plot\_shortLong +============================== + +.. currentmodule:: systole.plotly + +.. autofunction:: plot_shortLong \ No newline at end of file diff --git a/source/generated/plotly/systole.plotly.plot_subspaces.rst b/source/generated/plotly/systole.plotly.plot_subspaces.rst new file mode 100644 index 00000000..48fff1cf --- /dev/null +++ b/source/generated/plotly/systole.plotly.plot_subspaces.rst @@ -0,0 +1,6 @@ +systole.plotly.plot\_subspaces +============================== + +.. currentmodule:: systole.plotly + +.. autofunction:: plot_subspaces \ No newline at end of file diff --git a/source/generated/plotly/systole.plotly.plot_timedomain.rst b/source/generated/plotly/systole.plotly.plot_timedomain.rst new file mode 100644 index 00000000..4b480c98 --- /dev/null +++ b/source/generated/plotly/systole.plotly.plot_timedomain.rst @@ -0,0 +1,6 @@ +systole.plotly.plot\_timedomain +=============================== + +.. currentmodule:: systole.plotly + +.. autofunction:: plot_timedomain \ No newline at end of file diff --git a/source/generated/plotting/systole.plotting.circular.rst b/source/generated/plotting/systole.plotting.circular.rst new file mode 100644 index 00000000..8964ded8 --- /dev/null +++ b/source/generated/plotting/systole.plotting.circular.rst @@ -0,0 +1,6 @@ +systole.plotting.circular +========================= + +.. currentmodule:: systole.plotting + +.. autofunction:: circular \ No newline at end of file diff --git a/source/generated/plotting/systole.plotting.plot_circular.rst b/source/generated/plotting/systole.plotting.plot_circular.rst new file mode 100644 index 00000000..7d0d92eb --- /dev/null +++ b/source/generated/plotting/systole.plotting.plot_circular.rst @@ -0,0 +1,6 @@ +systole.plotting.plot\_circular +=============================== + +.. currentmodule:: systole.plotting + +.. autofunction:: plot_circular \ No newline at end of file diff --git a/source/generated/plotting/systole.plotting.plot_events.rst b/source/generated/plotting/systole.plotting.plot_events.rst new file mode 100644 index 00000000..eefc6665 --- /dev/null +++ b/source/generated/plotting/systole.plotting.plot_events.rst @@ -0,0 +1,6 @@ +systole.plotting.plot\_events +============================= + +.. currentmodule:: systole.plotting + +.. autofunction:: plot_events \ No newline at end of file diff --git a/source/generated/plotting/systole.plotting.plot_oximeter.rst b/source/generated/plotting/systole.plotting.plot_oximeter.rst new file mode 100644 index 00000000..57c361bb --- /dev/null +++ b/source/generated/plotting/systole.plotting.plot_oximeter.rst @@ -0,0 +1,6 @@ +systole.plotting.plot\_oximeter +=============================== + +.. currentmodule:: systole.plotting + +.. autofunction:: plot_oximeter \ No newline at end of file diff --git a/source/generated/plotting/systole.plotting.plot_psd.rst b/source/generated/plotting/systole.plotting.plot_psd.rst new file mode 100644 index 00000000..c0ebcdd2 --- /dev/null +++ b/source/generated/plotting/systole.plotting.plot_psd.rst @@ -0,0 +1,6 @@ +systole.plotting.plot\_psd +========================== + +.. currentmodule:: systole.plotting + +.. autofunction:: plot_psd \ No newline at end of file diff --git a/source/generated/plotting/systole.plotting.plot_raw.rst b/source/generated/plotting/systole.plotting.plot_raw.rst new file mode 100644 index 00000000..bd523ede --- /dev/null +++ b/source/generated/plotting/systole.plotting.plot_raw.rst @@ -0,0 +1,6 @@ +systole.plotting.plot\_raw +========================== + +.. currentmodule:: systole.plotting + +.. autofunction:: plot_raw \ No newline at end of file diff --git a/source/generated/plotting/systole.plotting.plot_subspaces.rst b/source/generated/plotting/systole.plotting.plot_subspaces.rst new file mode 100644 index 00000000..398647a1 --- /dev/null +++ b/source/generated/plotting/systole.plotting.plot_subspaces.rst @@ -0,0 +1,6 @@ +systole.plotting.plot\_subspaces +================================ + +.. currentmodule:: systole.plotting + +.. autofunction:: plot_subspaces \ No newline at end of file diff --git a/source/generated/recording/systole.recording.BrainVisionExG.rst b/source/generated/recording/systole.recording.BrainVisionExG.rst new file mode 100644 index 00000000..52630fde --- /dev/null +++ b/source/generated/recording/systole.recording.BrainVisionExG.rst @@ -0,0 +1,28 @@ +systole.recording.BrainVisionExG +================================ + +.. currentmodule:: systole.recording + +.. autoclass:: BrainVisionExG + + + .. automethod:: __init__ + + + .. rubric:: Methods + + .. autosummary:: + + ~BrainVisionExG.GetData + ~BrainVisionExG.GetProperties + ~BrainVisionExG.RecvData + ~BrainVisionExG.SplitString + ~BrainVisionExG.__init__ + ~BrainVisionExG.close + ~BrainVisionExG.read + + + + + + \ No newline at end of file diff --git a/source/generated/systole.recording.Oximeter.rst b/source/generated/recording/systole.recording.Oximeter.rst similarity index 77% rename from source/generated/systole.recording.Oximeter.rst rename to source/generated/recording/systole.recording.Oximeter.rst index 88596047..90c8ae39 100644 --- a/source/generated/systole.recording.Oximeter.rst +++ b/source/generated/recording/systole.recording.Oximeter.rst @@ -1,4 +1,4 @@ -systole.recording.Oximeter +systole.recording.Oximeter ========================== .. currentmodule:: systole.recording @@ -16,12 +16,15 @@ systole.recording.Oximeter ~Oximeter.__init__ ~Oximeter.add_paquet ~Oximeter.check + ~Oximeter.data_format2 + ~Oximeter.data_format7 ~Oximeter.find_peaks ~Oximeter.plot_events - ~Oximeter.plot_hr + ~Oximeter.plot_raw ~Oximeter.plot_recording ~Oximeter.read ~Oximeter.readInWaiting + ~Oximeter.reset ~Oximeter.save ~Oximeter.setup ~Oximeter.waitBeat diff --git a/source/generated/recording/systole.recording.findOximeter.rst b/source/generated/recording/systole.recording.findOximeter.rst new file mode 100644 index 00000000..62cf1e32 --- /dev/null +++ b/source/generated/recording/systole.recording.findOximeter.rst @@ -0,0 +1,6 @@ +systole.recording.findOximeter +============================== + +.. currentmodule:: systole.recording + +.. autofunction:: findOximeter \ No newline at end of file diff --git a/source/generated/systole.circular.rst b/source/generated/systole.circular.rst deleted file mode 100644 index a6edc6e4..00000000 --- a/source/generated/systole.circular.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.circular -================ - -.. currentmodule:: systole - -.. autofunction:: circular \ No newline at end of file diff --git a/source/generated/systole.frequency_domain.rst b/source/generated/systole.frequency_domain.rst deleted file mode 100644 index a4ecfbc0..00000000 --- a/source/generated/systole.frequency_domain.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.frequency\_domain -========================= - -.. currentmodule:: systole - -.. autofunction:: frequency_domain \ No newline at end of file diff --git a/source/generated/systole.heart_rate.rst b/source/generated/systole.heart_rate.rst deleted file mode 100644 index 40777aaf..00000000 --- a/source/generated/systole.heart_rate.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.heart\_rate -=================== - -.. currentmodule:: systole - -.. autofunction:: heart_rate \ No newline at end of file diff --git a/source/generated/systole.interpolate_clipping.rst b/source/generated/systole.interpolate_clipping.rst deleted file mode 100644 index c9f8d963..00000000 --- a/source/generated/systole.interpolate_clipping.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.interpolate\_clipping -============================= - -.. currentmodule:: systole - -.. autofunction:: interpolate_clipping \ No newline at end of file diff --git a/source/generated/systole.nnX.rst b/source/generated/systole.nnX.rst deleted file mode 100644 index bf4ccbe7..00000000 --- a/source/generated/systole.nnX.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.nnX -=========== - -.. currentmodule:: systole - -.. autofunction:: nnX \ No newline at end of file diff --git a/source/generated/systole.nonlinear.rst b/source/generated/systole.nonlinear.rst deleted file mode 100644 index d4081f5b..00000000 --- a/source/generated/systole.nonlinear.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.nonlinear -================= - -.. currentmodule:: systole - -.. autofunction:: nonlinear \ No newline at end of file diff --git a/source/generated/systole.norm_triggers.rst b/source/generated/systole.norm_triggers.rst deleted file mode 100644 index e4fa4bec..00000000 --- a/source/generated/systole.norm_triggers.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.norm\_triggers -====================== - -.. currentmodule:: systole - -.. autofunction:: norm_triggers \ No newline at end of file diff --git a/source/generated/systole.oxi_peaks.rst b/source/generated/systole.oxi_peaks.rst deleted file mode 100644 index fc727819..00000000 --- a/source/generated/systole.oxi_peaks.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.oxi\_peaks -================== - -.. currentmodule:: systole - -.. autofunction:: oxi_peaks \ No newline at end of file diff --git a/source/generated/systole.plot_circular.rst b/source/generated/systole.plot_circular.rst deleted file mode 100644 index 7cabd0dd..00000000 --- a/source/generated/systole.plot_circular.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.plot\_circular -====================== - -.. currentmodule:: systole - -.. autofunction:: plot_circular \ No newline at end of file diff --git a/source/generated/systole.plot_ectopic.rst b/source/generated/systole.plot_ectopic.rst deleted file mode 100644 index 2d62fdd0..00000000 --- a/source/generated/systole.plot_ectopic.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.plot\_ectopic -===================== - -.. currentmodule:: systole - -.. autofunction:: plot_ectopic \ No newline at end of file diff --git a/source/generated/systole.plot_events.rst b/source/generated/systole.plot_events.rst deleted file mode 100644 index 3b605e61..00000000 --- a/source/generated/systole.plot_events.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.plot\_events -==================== - -.. currentmodule:: systole - -.. autofunction:: plot_events \ No newline at end of file diff --git a/source/generated/systole.plot_hr.rst b/source/generated/systole.plot_hr.rst deleted file mode 100644 index af9568f7..00000000 --- a/source/generated/systole.plot_hr.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.plot\_hr -================ - -.. currentmodule:: systole - -.. autofunction:: plot_hr \ No newline at end of file diff --git a/source/generated/systole.plot_oximeter.rst b/source/generated/systole.plot_oximeter.rst deleted file mode 100644 index d838e83c..00000000 --- a/source/generated/systole.plot_oximeter.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.plot\_oximeter -====================== - -.. currentmodule:: systole - -.. autofunction:: plot_oximeter \ No newline at end of file diff --git a/source/generated/systole.plot_raw.rst b/source/generated/systole.plot_raw.rst deleted file mode 100644 index 3f4bf26c..00000000 --- a/source/generated/systole.plot_raw.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.plot\_raw -================= - -.. currentmodule:: systole - -.. autofunction:: plot_raw \ No newline at end of file diff --git a/source/generated/systole.plot_shortLong.rst b/source/generated/systole.plot_shortLong.rst deleted file mode 100644 index 3a401275..00000000 --- a/source/generated/systole.plot_shortLong.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.plot\_shortLong -======================= - -.. currentmodule:: systole - -.. autofunction:: plot_shortLong \ No newline at end of file diff --git a/source/generated/systole.plot_subspaces.rst b/source/generated/systole.plot_subspaces.rst deleted file mode 100644 index 321f6c4f..00000000 --- a/source/generated/systole.plot_subspaces.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.plot\_subspaces -======================= - -.. currentmodule:: systole - -.. autofunction:: plot_subspaces \ No newline at end of file diff --git a/source/generated/systole.pnnX.rst b/source/generated/systole.pnnX.rst deleted file mode 100644 index acf4ab30..00000000 --- a/source/generated/systole.pnnX.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.pnnX -============ - -.. currentmodule:: systole - -.. autofunction:: pnnX \ No newline at end of file diff --git a/source/generated/systole.rmssd.rst b/source/generated/systole.rmssd.rst deleted file mode 100644 index f20ed7bc..00000000 --- a/source/generated/systole.rmssd.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.rmssd -============= - -.. currentmodule:: systole - -.. autofunction:: rmssd \ No newline at end of file diff --git a/source/generated/systole.rr_artefacts.rst b/source/generated/systole.rr_artefacts.rst deleted file mode 100644 index 530df7c4..00000000 --- a/source/generated/systole.rr_artefacts.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.rr\_artefacts -===================== - -.. currentmodule:: systole - -.. autofunction:: rr_artefacts \ No newline at end of file diff --git a/source/generated/systole.time_domain.rst b/source/generated/systole.time_domain.rst deleted file mode 100644 index 5438b262..00000000 --- a/source/generated/systole.time_domain.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.time\_domain -==================== - -.. currentmodule:: systole - -.. autofunction:: time_domain \ No newline at end of file diff --git a/source/generated/systole.time_shift.rst b/source/generated/systole.time_shift.rst deleted file mode 100644 index bcb71491..00000000 --- a/source/generated/systole.time_shift.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.time\_shift -=================== - -.. currentmodule:: systole - -.. autofunction:: time_shift \ No newline at end of file diff --git a/source/generated/systole.to_angles.rst b/source/generated/systole.to_angles.rst deleted file mode 100644 index 01848bb6..00000000 --- a/source/generated/systole.to_angles.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.to\_angles -================== - -.. currentmodule:: systole - -.. autofunction:: to_angles \ No newline at end of file diff --git a/source/generated/systole.to_epochs.rst b/source/generated/systole.to_epochs.rst deleted file mode 100644 index d36de5e2..00000000 --- a/source/generated/systole.to_epochs.rst +++ /dev/null @@ -1,6 +0,0 @@ -systole.to\_epochs -================== - -.. currentmodule:: systole - -.. autofunction:: to_epochs \ No newline at end of file diff --git a/source/generated/utils/systole.utils.heart_rate.rst b/source/generated/utils/systole.utils.heart_rate.rst new file mode 100644 index 00000000..af63f6fb --- /dev/null +++ b/source/generated/utils/systole.utils.heart_rate.rst @@ -0,0 +1,6 @@ +systole.utils.heart\_rate +========================= + +.. currentmodule:: systole.utils + +.. autofunction:: heart_rate \ No newline at end of file diff --git a/source/generated/utils/systole.utils.norm_triggers.rst b/source/generated/utils/systole.utils.norm_triggers.rst new file mode 100644 index 00000000..a7f58766 --- /dev/null +++ b/source/generated/utils/systole.utils.norm_triggers.rst @@ -0,0 +1,6 @@ +systole.utils.norm\_triggers +============================ + +.. currentmodule:: systole.utils + +.. autofunction:: norm_triggers \ No newline at end of file diff --git a/source/generated/utils/systole.utils.simulate_rr.rst b/source/generated/utils/systole.utils.simulate_rr.rst new file mode 100644 index 00000000..5920ea7e --- /dev/null +++ b/source/generated/utils/systole.utils.simulate_rr.rst @@ -0,0 +1,6 @@ +systole.utils.simulate\_rr +========================== + +.. currentmodule:: systole.utils + +.. autofunction:: simulate_rr \ No newline at end of file diff --git a/source/generated/utils/systole.utils.time_shift.rst b/source/generated/utils/systole.utils.time_shift.rst new file mode 100644 index 00000000..3e732e68 --- /dev/null +++ b/source/generated/utils/systole.utils.time_shift.rst @@ -0,0 +1,6 @@ +systole.utils.time\_shift +========================= + +.. currentmodule:: systole.utils + +.. autofunction:: time_shift \ No newline at end of file diff --git a/source/generated/utils/systole.utils.to_angles.rst b/source/generated/utils/systole.utils.to_angles.rst new file mode 100644 index 00000000..21aa802e --- /dev/null +++ b/source/generated/utils/systole.utils.to_angles.rst @@ -0,0 +1,6 @@ +systole.utils.to\_angles +======================== + +.. currentmodule:: systole.utils + +.. autofunction:: to_angles \ No newline at end of file diff --git a/source/generated/utils/systole.utils.to_epochs.rst b/source/generated/utils/systole.utils.to_epochs.rst new file mode 100644 index 00000000..36fd777e --- /dev/null +++ b/source/generated/utils/systole.utils.to_epochs.rst @@ -0,0 +1,6 @@ +systole.utils.to\_epochs +======================== + +.. currentmodule:: systole.utils + +.. autofunction:: to_epochs \ No newline at end of file diff --git a/source/generated/utils/systole.utils.to_neighbour.rst b/source/generated/utils/systole.utils.to_neighbour.rst new file mode 100644 index 00000000..50ccbedd --- /dev/null +++ b/source/generated/utils/systole.utils.to_neighbour.rst @@ -0,0 +1,6 @@ +systole.utils.to\_neighbour +=========================== + +.. currentmodule:: systole.utils + +.. autofunction:: to_neighbour \ No newline at end of file diff --git a/source/generated/utils/systole.utils.to_rr.rst b/source/generated/utils/systole.utils.to_rr.rst new file mode 100644 index 00000000..30a18028 --- /dev/null +++ b/source/generated/utils/systole.utils.to_rr.rst @@ -0,0 +1,6 @@ +systole.utils.to\_rr +==================== + +.. currentmodule:: systole.utils + +.. autofunction:: to_rr \ No newline at end of file diff --git a/source/index.rst b/source/index.rst index 8dbe2fec..e84b85bd 100644 --- a/source/index.rst +++ b/source/index.rst @@ -14,6 +14,18 @@ .. image:: https://codecov.io/gh/embodied-computation-group/systole/branch/master/graph/badge.svg :target: https://codecov.io/gh/embodied-computation-group/systole +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + +.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 + :target: https://pycqa.github.io/isort/ + +.. image:: http://www.mypy-lang.org/static/mypy_badge.svg + :target: http://mypy-lang.org/ + +.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white + :target: https://github.com/pre-commit/pre-commit + ================ .. figure:: https://github.com/embodied-computation-group/systole/raw/master/source/images/banner.png @@ -38,102 +50,44 @@ Systole can be installed using pip: The following packages are required to use Systole: -* Numpy (>=1.15) -* SciPy (>=1.3.0) -* Pandas (>=0.24) -* Matplotlib (>=3.0.2) -* Seaborn (>=0.9.0) - -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 +* `Numpy `_ (>=1.15) +* `SciPy `_ (>=1.3.0) +* `Pandas `_ (>=0.24) +* `Matplotlib `_ (>=3.0.2) +* `Seaborn `_ (>=0.9.0) +* `py-ecg-detectors `_ (>=1.0.2) -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. +Interactive plotting functions and reports generation will also require the following packages to be installed: -.. code-block:: python - - # Code 1 {} - oximeter.read(duration=10) - # Code 2 {} - -* The ``readInWaiting()`` method +* `Plotly `_ (>=4.8.0) -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 +Tutorial +======== - 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 +For an overview of all the recording functionalities, you can refer to the following examples: - import serial - import time - from systole.recording import Oximeter +* Recording +* Artefacts detection and artefacts correction +* Heart rate variability - # Open serial port - ser = serial.Serial('COM4') # Change this value according to your setup +For an introduction to Systole and cardiac signal analysis, you can refer to the following tutorial: - # Create an Oxymeter instance and initialize recording - oxi = Oximeter(serial=ser, sfreq=75, add_channels=4).setup() +* Introduction to cardiac signal analysis - |Colab badge| - `Jupyter Book `_ - # 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') +.. |Colab badge| image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/github/LegrandNico/Notebooks/blob/main/IntroductionCardiacSignalAnalysis.ipynb -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 =================== -Systole implements the artefact rejection method recently proposed by Lipponen & Tarvainen (2019) [#]_. +Systole implements systolic peak detection inspired by van Gent et al. (2019) [#]_ and the artefact rejection method recently proposed by Lipponen & Tarvainen (2019) [#]_. .. code-block:: python @@ -204,7 +158,7 @@ References **Peak detection (PPG signal)** -.. [#] van Gent, P., Farah, H., van Nes, N., & van Arem, B. (2019). HeartPy: A novel heart rate algorithm for the analysis of noisy signals. Transportation Research Part F: Traffic Psychology and Behaviour, 66, 368–378. https://doi.org/10.1016/j.trf.2019.09.015 +.. [#] van Gent, P., Farah, H., van Nes, N., & van Arem, B. (2019). HeartPy: A novel heart rate algorithm for the analysis of noisy signals. *Transportation Research Part F: Traffic Psychology and Behaviour, 66, 368–378*. https://doi.org/10.1016/j.trf.2019.09.015 **Artefact detection and correction:** diff --git a/systole/__init__.py b/systole/__init__.py index 957c2931..fd7af8e6 100644 --- a/systole/__init__.py +++ b/systole/__init__.py @@ -1,9 +1,14 @@ +# Raise a warning if a newer version of Systole is available +from outdated import warn_if_outdated + +from .correction import * +from .datasets import * from .detection import * -from .utils import * -from .plotting import * from .hrv import * -from .datasets import * -from .correction import * from .plotly import * +from .plotting import * # type: ignore +from .utils import * + +__version__ = "0.1.3" -__version__ = "0.1.2" +warn_if_outdated("systole", __version__) diff --git a/systole/correction.py b/systole/correction.py index b36ed49a..8433ef24 100644 --- a/systole/correction.py +++ b/systole/correction.py @@ -1,23 +1,26 @@ # Author: Nicolas Legrand +from typing import Dict, List, Union + import numpy as np from scipy.interpolate import interp1d + from systole.detection import rr_artefacts -def correct_extra(rr, idx): +def correct_extra(rr: Union[List, np.ndarray], idx: int) -> np.ndarray: """Correct extra beats by removing the RR interval. Parameters ---------- - rr : 1d array-like + rr : np.ndarray or list RR intervals. idx : int Index of the extra RR interval. Returns ------- - clean_rr : 1d array-like + clean_rr : np.ndarray Corrected RR intervals. """ if isinstance(rr, list): @@ -26,29 +29,29 @@ def correct_extra(rr, idx): clean_rr = rr if idx == len(clean_rr): - clean_rr = np.delete(clean_rr, idx-1) + clean_rr = np.delete(clean_rr, idx - 1) else: # Add the extra interval to the next one - clean_rr[idx+1] = clean_rr[idx+1] + clean_rr[idx] + clean_rr[idx + 1] = clean_rr[idx + 1] + clean_rr[idx] # Remove current interval clean_rr = np.delete(clean_rr, idx) return clean_rr -def correct_missed(rr, idx): +def correct_missed(rr: Union[List, np.ndarray], idx: int) -> np.ndarray: """Correct missed beats by adding a new RR interval. Parameters ---------- - rr : 1d array-like + rr : np.ndarray or list RR intervals. idx : int Index of the missed RR interval. Returns ------- - clean_rr : 1d array-like + clean_rr : np.ndarray Corrected RR intervals. """ if isinstance(rr, list): @@ -65,19 +68,21 @@ def correct_missed(rr, idx): return clean_rr -def interpolate_bads(rr, idx): +def interpolate_bads( + rr: Union[List, np.ndarray], idx: Union[int, List, np.ndarray] +) -> np.ndarray: """Correct long and short beats using interpolation. Parameters ---------- - rr : 1d array-like + rr : np.ndarray or list RR intervals (ms). - idx : int or 1d array-like + idx : int, np.ndarray or list Index of the RR interval to correct. Returns ------- - clean_rr : 1d array-like + clean_rr : np.ndarray Corrected RR intervals. """ if isinstance(rr, list): @@ -92,9 +97,14 @@ def interpolate_bads(rr, idx): return clean_rr -def correct_rr(rr, extra_correction=True, missed_correction=True, - short_correction=True, long_correction=True, - ectopic_correction=True): +def correct_rr( + rr: Union[List, np.ndarray], + extra_correction: bool = True, + missed_correction: bool = True, + short_correction: bool = True, + long_correction: bool = True, + ectopic_correction: bool = True, +) -> Dict[str, Union[int, np.ndarray]]: """Correct long and short beats using interpolation. Parameters @@ -102,15 +112,15 @@ def correct_rr(rr, extra_correction=True, missed_correction=True, rr : 1d array-like RR intervals (ms). correct_extra : boolean - If True, correct extra beats in the RR time series. + If `True`, correct extra beats in the RR time series. correct_missed : boolean - If True, correct missed beats in the RR time series. + If `True`, correct missed beats in the RR time series. correct_short : boolean - If True, correct short beats in the RR time series. + If `True`, correct short beats in the RR time series. correct_long : boolean - If True, correct long beats in the RR time series. + If `True`, correct long beats in the RR time series. correct_ectopic : boolean - If True, correct ectopic beats in the RR time series. + If `True`, correct ectopic beats in the RR time series. Returns ------- @@ -130,8 +140,7 @@ def correct_rr(rr, extra_correction=True, missed_correction=True, * missed: int The number of missed beats corrected. """ - if isinstance(rr, list): - rr = np.asarray(rr) + rr = np.asarray(rr) clean_rr = rr.copy() nEctopic, nShort, nLong, nExtra, nMissed = 0, 0, 0, 0, 0 @@ -140,8 +149,8 @@ def correct_rr(rr, extra_correction=True, missed_correction=True, # Correct missed beats if missed_correction: - if np.any(artefacts['missed']): - for this_id in np.where(artefacts['missed'])[0]: + if np.any(artefacts["missed"]): + for this_id in np.where(artefacts["missed"])[0]: this_id += nMissed clean_rr = correct_missed(clean_rr, this_id) nMissed += 1 @@ -149,8 +158,8 @@ def correct_rr(rr, extra_correction=True, missed_correction=True, # Correct extra beats if extra_correction: - if np.any(artefacts['extra']): - for this_id in np.where(artefacts['extra'])[0]: + if np.any(artefacts["extra"]): + for this_id in np.where(artefacts["extra"])[0]: this_id -= nExtra clean_rr = correct_missed(clean_rr, this_id) nExtra += 1 @@ -158,36 +167,44 @@ def correct_rr(rr, extra_correction=True, missed_correction=True, # Correct ectopic beats if ectopic_correction: - if np.any(artefacts['ectopic']): + if np.any(artefacts["ectopic"]): # Also correct the beat before - for i in np.where(artefacts['ectopic'])[0]: - if (i > 0) & (i < len(artefacts['ectopic'])): - artefacts['ectopic'][i-1] = True - this_id = np.where(artefacts['ectopic'])[0] + for i in np.where(artefacts["ectopic"])[0]: + if (i > 0) & (i < len(artefacts["ectopic"])): + artefacts["ectopic"][i - 1] = True + this_id = np.where(artefacts["ectopic"])[0] clean_rr = interpolate_bads(clean_rr, [this_id]) - nEctopic = np.sum(artefacts['ectopic']) + nEctopic = np.sum(artefacts["ectopic"]) # type: ignore # Correct short beats if short_correction: - if np.any(artefacts['short']): - this_id = np.where(artefacts['short'])[0] + if np.any(artefacts["short"]): + this_id = np.where(artefacts["short"])[0] clean_rr = interpolate_bads(clean_rr, this_id) nShort = len(this_id) # Correct long beats if long_correction: - if np.any(artefacts['long']): - this_id = np.where(artefacts['long'])[0] + if np.any(artefacts["long"]): + this_id = np.where(artefacts["long"])[0] clean_rr = interpolate_bads(clean_rr, this_id) nLong = len(this_id) - return {'clean_rr': clean_rr, 'ectopic': nEctopic, 'short': nShort, - 'long': nLong, 'extra': nExtra, 'missed': nMissed} - - -def correct_peaks(peaks, extra_correction=True, missed_correction=True, - short_correction=True, long_correction=True, - ectopic_correction=True): + return { + "clean_rr": clean_rr, + "ectopic": nEctopic, + "short": nShort, + "long": nLong, + "extra": nExtra, + "missed": nMissed, + } + + +def correct_peaks( + peaks: Union[List, np.ndarray], + extra_correction: bool = True, + missed_correction: bool = True, +) -> Dict[str, Union[int, np.ndarray]]: """Correct long, short, extra, missed and ectopic beats in peaks vector. Parameters @@ -223,8 +240,8 @@ def correct_peaks(peaks, extra_correction=True, missed_correction=True, # Correct missed beats if missed_correction: - if np.any(artefacts['missed']): - for this_id in np.where(artefacts['missed'])[0]: + if np.any(artefacts["missed"]): + for this_id in np.where(artefacts["missed"])[0]: this_id += nMissed clean_peaks = correct_missed_peaks(clean_peaks, this_id) nMissed += 1 @@ -232,18 +249,24 @@ def correct_peaks(peaks, extra_correction=True, missed_correction=True, # Correct extra beats if extra_correction: - if np.any(artefacts['extra']): - for this_id in np.where(artefacts['extra'])[0]: + if np.any(artefacts["extra"]): + for this_id in np.where(artefacts["extra"])[0]: this_id -= nExtra clean_peaks = correct_extra_peaks(clean_peaks, this_id) nExtra += 1 artefacts = rr_artefacts(np.diff(np.where(clean_peaks)[0])) - return {'clean_peaks': clean_peaks, 'ectopic': nEctopic, 'short': nShort, - 'long': nLong, 'extra': nExtra, 'missed': nMissed} + return { + "clean_peaks": clean_peaks, + "ectopic": nEctopic, + "short": nShort, + "long": nLong, + "extra": nExtra, + "missed": nMissed, + } -def correct_missed_peaks(peaks, idx): +def correct_missed_peaks(peaks: Union[List, np.ndarray], idx: int) -> np.ndarray: """Correct missed beats by adding a new RR interval. Parameters @@ -265,15 +288,15 @@ def correct_missed_peaks(peaks, idx): index = np.where(clean_peaks)[0] # Estimate new interval - interval = int(round((index[idx+1] - index[idx])/2)) + interval = int(round((index[idx + 1] - index[idx]) / 2)) # Add peak in vector - clean_peaks[index[idx]+interval] = True + clean_peaks[index[idx] + interval] = True return clean_peaks -def correct_extra_peaks(peaks, idx): +def correct_extra_peaks(peaks: Union[List, np.ndarray], idx: int) -> np.ndarray: """Correct extra beats by removing peak. Parameters diff --git a/systole/datasets/Task1_ECG.npy b/systole/datasets/Task1_ECG.npy index 4838024a..1ebf3bd7 100644 Binary files a/systole/datasets/Task1_ECG.npy and b/systole/datasets/Task1_ECG.npy differ diff --git a/systole/datasets/Task1_EDA.npy b/systole/datasets/Task1_EDA.npy index 073b47b6..2131e408 100644 Binary files a/systole/datasets/Task1_EDA.npy and b/systole/datasets/Task1_EDA.npy differ diff --git a/systole/datasets/Task1_Respiration.npy b/systole/datasets/Task1_Respiration.npy index cfdaa9fb..9915f4de 100644 Binary files a/systole/datasets/Task1_Respiration.npy and b/systole/datasets/Task1_Respiration.npy differ diff --git a/systole/datasets/Task1_Stim.npy b/systole/datasets/Task1_Stim.npy index 116eea9e..84967fa2 100644 Binary files a/systole/datasets/Task1_Stim.npy and b/systole/datasets/Task1_Stim.npy differ diff --git a/systole/datasets/__init__.py b/systole/datasets/__init__.py index 41d535dc..625dc9a1 100644 --- a/systole/datasets/__init__.py +++ b/systole/datasets/__init__.py @@ -1,16 +1,23 @@ +# Author: Nicolas Legrand + +import io +import os.path as op import time +from typing import List + import numpy as np import pandas as pd -import os.path as op +import requests +from tqdm import tqdm ddir = op.dirname(op.realpath(__file__)) -__all__ = ["import_ppg", "import_rr", "serialSim", "import_dataset"] +__all__ = ["import_ppg", "import_rr", "serialSim", "import_dataset1"] # Simulate serial inputs from ppg recording # ========================================= -class serialSim(): +class serialSim: """Simulate online data acquisition using pre recorded signal and realistic sampling rate (75 Hz). """ @@ -45,10 +52,10 @@ def read(self, lenght): return paquet[0], paquet[1], paquet[2], paquet[3], paquet[4] def reset_input_buffer(self): - print('Reset input buffer') + print("Reset input buffer") -def import_ppg(): +def import_ppg() -> pd.DataFrame: """Import a 5 minutes long PPG recording. Returns @@ -56,13 +63,20 @@ def import_ppg(): df : :py:class:`pandas.DataFrame` Dataframe containing the PPG signale. """ - df = pd.DataFrame({'ppg': np.load(op.join(ddir, 'ppg.npy'))}) - df['time'] = np.arange(0, len(df))/75 + path = ( + "https://github.com/embodied-computation-group/systole/raw/" + "master/systole/datasets/" + ) + response = requests.get(f"{path}/ppg.npy") + response.raise_for_status() + ppg = np.load(io.BytesIO(response.content), allow_pickle=True) + df = pd.DataFrame({"ppg": ppg}) + df["time"] = np.arange(0, len(df)) / 75 return df -def import_rr(): +def import_rr() -> pd.DataFrame: """Import PPG recording. Returns @@ -70,13 +84,19 @@ def import_rr(): rr : :py:class:`pandas.DataFrame` Dataframe containing the RR time-serie. """ - rr = pd.read_csv(op.join(ddir, 'rr.txt')) + path = ( + "https://github.com/embodied-computation-group/systole/raw/" + "master/systole/datasets/" + ) + rr = pd.read_csv(op.join(path, "rr.txt")) return rr -def import_dataset(): - """Import PPG recording. +def import_dataset1( + modalities: List[str] = ["ECG", "EDA", "Respiration", "Stim"] +) -> pd.DataFrame: + """Import ECG, EDA and respiration recording. Returns ------- @@ -87,20 +107,26 @@ def import_dataset(): ----- Load a 20 minutes recording of ECG, EDA and respiration of a young healthy participant undergoing the emotional task (valence rating of neutral and - disgusting images) described in _[1]. + disgusting images) described in _[1]. The sampling frequency is 1000 Hz. References ---------- [1] : Legrand, N., Etard, O., Vandevelde, A., Pierre, M., Viader, F., - Clochon, P., Doidy, F., Peschanski, D., Eustache, F. & Gagnepain, P. - (2018). Preprint version 3.0. - doi: https://www.biorxiv.org/content/10.1101/376954v3 + Clochon, P., Doidy, F., Peschanski, D., Eustache, F., & + Gagnepain, P. (2020). Long-term modulation of cardiac activity induced + by inhibitory control over emotional memories. Scientific Reports, + 10(1). https://doi.org/10.1038/s41598-020-71858-2 """ - df = pd.DataFrame({ - 'ecg': np.load(op.join(ddir, 'Task1_ECG.npy')), - 'eda': np.load(op.join(ddir, 'Task1_EDA.npy')), - 'respiration': np.load(op.join(ddir, 'Task1_Respiration.npy')), - 'stim': np.load(op.join(ddir, 'Task1_Stim.npy'))}) - df['time'] = np.arange(0, len(df))/1000 + path = "https://github.com/embodied-computation-group/systole/raw/dev/systole/datasets/Task1_" + pbar = tqdm(modalities, position=0, leave=True) + data = {} + for item in pbar: + pbar.set_description(f"Downloading {item} channel") + response = requests.get(f"{path}{item}.npy") + response.raise_for_status() + data[item.lower()] = np.load(io.BytesIO(response.content), allow_pickle=True) + + df = pd.DataFrame(data) + df["time"] = np.arange(0, len(df)) / 1000 return df diff --git a/systole/datasets/__pycache__/__init__.cpython-37.pyc b/systole/datasets/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 50d55234..00000000 Binary files a/systole/datasets/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/systole/detection.py b/systole/detection.py index 9d7e1dd3..145021f4 100644 --- a/systole/detection.py +++ b/systole/detection.py @@ -1,30 +1,54 @@ # Author: Nicolas Legrand +from typing import Dict, List, Tuple, Union + import numpy as np import pandas as pd +from ecgdetectors import Detectors from scipy.interpolate import interp1d from scipy.signal import find_peaks -from ecgdetectors import Detectors + from systole.utils import to_neighbour -def oxi_peaks(x, sfreq=75, win=1, new_sfreq=1000, clipping=True, - noise_removal=True, peak_enhancement=True): - """A simple peak finder for PPG signal. +def oxi_peaks( + x: Union[List, np.ndarray], + sfreq: int = 75, + win: float = 0.75, + new_sfreq: int = 1000, + clipping: bool = True, + noise_removal: bool = True, + peak_enhancement: bool = True, + distance: float = 0.3, + clean_extra: bool = False, +) -> Tuple[np.ndarray, np.ndarray]: + """A simple systolic peak finder for PPG signals. + + This method uses a rolling average + standard deviation + approach to update a detection threshold. All the peaks found + above this threshold are potential systolic peaks. Parameters ---------- - x : list or 1d array-like - The oxi signal. + x : np.ndarray or list + The pulse oximeter time series. sfreq : int The sampling frequency. Default is set to 75 Hz. win : int - Window size (in seconds) used to compute the threshold. + Window size (in seconds) used to compute the threshold (i.e. + rolling mean + standard deviation). new_sfreq : int If resample is *True*, the new sampling frequency. - resample : bool - If *True* (defaults), will resample the signal at *new_sfreq*. Default + resample : boolean + If `True` (default), will resample the signal at *new_sfreq*. Default value is 1000 Hz. + peak_enhancement : boolean + If `True` (default), the ppg signal is squared before peaks detection. + distance : float + The minimum interval between two peaks (seconds). + clean_extra : bool + If `True`, use `:py:func:systole.detection.rr_artefacts()` to find and + remove extra peaks. Default is `False`. Returns ------- @@ -37,10 +61,11 @@ def oxi_peaks(x, sfreq=75, win=1, new_sfreq=1000, clipping=True, ----- This algorithm use a simple rolling average to detect peaks. The signal is first resampled and a rolling average is applyed to correct high frequency - noise and clipping. The signal is then squared and detection of peaks is - performed using threshold set by the moving averagte + stadard deviation. + noise and clipping, using method detailled in [1]_. The signal is then + squared and detection of peaks is performed using threshold corresponding + to the moving averagte + stadard deviation. - .. warning :: This function will resample the signal to 1000 Hz. + .. warning :: This function will resample the signal to 1000 Hz by default. Examples -------- @@ -57,16 +82,13 @@ def oxi_peaks(x, sfreq=75, win=1, new_sfreq=1000, clipping=True, 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) + """ + x = np.asarray(x) # Interpolate - f = interp1d(np.arange(0, len(x)/sfreq, 1/sfreq), - x, - fill_value="extrapolate") - time = np.arange(0, len(x)/sfreq, 1/new_sfreq) + f = interp1d(np.arange(0, len(x) / sfreq, 1 / sfreq), x, fill_value="extrapolate") + time = np.arange(0, len(x) / sfreq, 1 / new_sfreq) x = f(time) # Copy resampled signal for output @@ -78,61 +100,83 @@ def oxi_peaks(x, sfreq=75, win=1, new_sfreq=1000, clipping=True, if noise_removal is True: # Moving average (high frequency noise + clipping) - rollingNoise = int(new_sfreq*.05) # 0.5 second window - x = pd.DataFrame( - {'signal': x}).rolling(rollingNoise, - center=True).mean().signal.to_numpy() + rollingNoise = int(new_sfreq * 0.05) # 0.05 second window + x = ( + pd.DataFrame({"signal": x}) + .rolling(rollingNoise, center=True) + .mean() + .signal.to_numpy() + ) if peak_enhancement is True: # Square signal (peak enhancement) - x = x ** 2 + x = np.asarray(x) ** 2 # Compute moving average and standard deviation - signal = pd.DataFrame({'signal': x}) - mean_signal = signal.rolling(int(new_sfreq*0.75), - center=True).mean().signal.to_numpy() - std_signal = signal.rolling(int(new_sfreq*0.75), - center=True).std().signal.to_numpy() + signal = pd.DataFrame({"signal": x}) + mean_signal = ( + signal.rolling(int(new_sfreq * win), center=True).mean().signal.to_numpy() + ) + std_signal = ( + signal.rolling(int(new_sfreq * win), center=True).std().signal.to_numpy() + ) # Substract moving average + standard deviation - x -= (mean_signal + std_signal) + x -= mean_signal + std_signal # Find positive peaks - peaks_idx = find_peaks(x, height=0, distance=int(new_sfreq*0.2))[0] + peaks_idx = find_peaks(x, height=0, distance=int(new_sfreq * distance))[0] # Create boolean vector peaks = np.zeros(len(x), dtype=bool) peaks[peaks_idx] = 1 + # Remove extra peaks + if clean_extra: + + # Search artefacts + rr = np.diff(np.where(peaks)[0]) # Convert to RR time series + artefacts = rr_artefacts(rr) + + # Clean peak vector + peaks[peaks_idx[1:][artefacts["extra"]]] = 0 + return resampled_signal, peaks -def ecg_peaks(x, sfreq=1000, new_sfreq=1000, method='pan-tompkins', - find_local=True, win_size=100): +def ecg_peaks( + x: Union[List, np.ndarray], + sfreq: int = 1000, + new_sfreq: int = 1000, + method: str = "pan-tompkins", + find_local: bool = True, + win_size: float = 0.1, +) -> Tuple[np.ndarray, np.ndarray]: """A simple wrapper for many popular R peaks detectors algorithms. - This function calls methods from the py-ecg-detectors [#]_ module. + This function calls methods from `py-ecg-detectors` [1]_. Parameters ---------- - x : list or 1d array-like + x : np.ndarray or list The oxi signal. sfreq : int The sampling frequency. Default is set to 75 Hz. method : str - The method used. Can be one of the following: 'hamilton', 'christov', - 'engelse-zeelenberg', 'pan-tompkins', 'wavelet-transform', - 'moving-average'. + The method used. Can be one of the following: `'hamilton'`, + `'christov'`, `'engelse-zeelenberg'`, `'pan-tompkins'`, + `'wavelet-transform'`, `'moving-average'`. find_local : bool If *True*, will use peaks indexs to search for local peaks given the window size (win_size). win_size : int Size of the time window used by :py:func:`systole.utils.to_neighbour()` + expressed in seconds. Defaut set to `0.1`. Returns ------- - peaks : 1d array-like + peaks : np.ndarray Numpy array containing peaks index. - resampled_signal : 1d array-like + resampled_signal : np.ndarray Signal resampled to the `new_sfreq` frequency. Notes @@ -154,18 +198,16 @@ def ecg_peaks(x, sfreq=1000, new_sfreq=1000, method='pan-tompkins', References ---------- - .. [#] Howell, L., Porr, B. Popular ECG R peak detectors written in + .. [1] 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) # Interpolate - f = interp1d(np.arange(0, len(x)/sfreq, 1/sfreq), - x, - fill_value="extrapolate") - time = np.arange(0, len(x)/sfreq, 1/new_sfreq) + f = interp1d(np.arange(0, len(x) / sfreq, 1 / sfreq), x, fill_value="extrapolate") + time = np.arange(0, len(x) / sfreq, 1 / new_sfreq) x = f(time) # Copy resampled signal for output @@ -173,33 +215,36 @@ def ecg_peaks(x, sfreq=1000, new_sfreq=1000, method='pan-tompkins', detectors = Detectors(new_sfreq) - if method == 'hamilton': + if method == "hamilton": peaks_idx = detectors.hamilton_detector(resampled_signal) - elif method == 'christov': + elif method == "christov": peaks_idx = detectors.christov_detector(resampled_signal) - elif method == 'engelse-zeelenberg': + elif method == "engelse-zeelenberg": peaks_idx = detectors.engzee_detector(resampled_signal) - elif method == 'pan-tompkins': + elif method == "pan-tompkins": peaks_idx = detectors.pan_tompkins_detector(resampled_signal) - elif method == 'wavelet-transform': + elif method == "wavelet-transform": peaks_idx = detectors.swt_detector(resampled_signal) - elif method == 'moving-average': + elif method == "moving-average": peaks_idx = detectors.two_average_detector(resampled_signal) else: raise ValueError( - 'Invalid method provided, should be: hamilton,', - 'christov, engelse-zeelenberg, pan-tompkins, wavelet-transform,', - 'moving-average') + "Invalid method provided, should be: hamilton, " + "christov, engelse-zeelenberg, pan-tompkins, wavelet-transform, " + "moving-average" + ) peaks = np.zeros(len(resampled_signal), dtype=bool) peaks[peaks_idx] = True if find_local is True: - peaks = to_neighbour(resampled_signal, peaks, size=win_size) + peaks = to_neighbour(resampled_signal, peaks, size=int(win_size * new_sfreq)) return resampled_signal, peaks -def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2): +def rr_artefacts( + rr: Union[List, np.ndarray], c1: float = 0.13, c2: float = 0.17, alpha: float = 5.2 +) -> Dict[str, np.ndarray]: """Artefacts detection from RR time series using the subspaces approach proposed by Lipponen & Tarvainen (2019). @@ -209,16 +254,16 @@ def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2): Array of RR intervals. c1 : float Fixed variable controling the slope of the threshold lines. Default is - 0.13. + `0.13`. c2 : float Fixed variable controling the intersect of the threshold lines. Default - is 0.17. + is `0.17`. alpha : float Scaling factor used to normalize the RR intervals first deviation. Returns ------- - artefacts : dictionnary + artefacts : dict Dictionnary storing the parameters of RR artefacts rejection. All the vectors outputed have the same length as the provided RR time serie: @@ -247,7 +292,7 @@ def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2): Notes ----- - This function will use the method proposed by [#]_ to detect ectopic beats, + This function will use the method proposed by [1]_ to detect ectopic beats, long, shorts, missed and extra RR intervals. Examples @@ -262,7 +307,7 @@ def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2): References ---------- - .. [#] Lipponen, J. A., & Tarvainen, M. P. (2019). A robust algorithm for + .. [1] 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 @@ -278,44 +323,48 @@ def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2): dRR = np.diff(rr, prepend=0) dRR[0] = dRR[1:].mean() # Set first item to a realistic value - dRR_df = pd.DataFrame({'signal': np.abs(dRR)}) - q1 = dRR_df.rolling( - 91, center=True, min_periods=1).quantile(.25).signal.to_numpy() - q3 = dRR_df.rolling( - 91, center=True, min_periods=1).quantile(.75).signal.to_numpy() + dRR_df = pd.DataFrame({"signal": np.abs(dRR)}) + q1 = dRR_df.rolling(91, center=True, min_periods=1).quantile(0.25).signal.to_numpy() + q3 = dRR_df.rolling(91, center=True, min_periods=1).quantile(0.75).signal.to_numpy() th1 = alpha * ((q3 - q1) / 2) dRR = dRR / th1 s11 = dRR # mRRs time serie - medRR = pd.DataFrame({'signal': rr}).rolling( - 11, center=True, min_periods=1).median().signal.to_numpy() + medRR = ( + pd.DataFrame({"signal": rr}) + .rolling(11, center=True, min_periods=1) + .median() + .signal.to_numpy() + ) mRR = rr - medRR mRR[mRR < 0] = 2 * mRR[mRR < 0] - mRR_df = pd.DataFrame({'signal': np.abs(mRR)}) - q1 = mRR_df.rolling( - 91, center=True, min_periods=1).quantile(.25).signal.to_numpy() - q3 = mRR_df.rolling( - 91, center=True, min_periods=1).quantile(.75).signal.to_numpy() + mRR_df = pd.DataFrame({"signal": np.abs(mRR)}) + q1 = mRR_df.rolling(91, center=True, min_periods=1).quantile(0.25).signal.to_numpy() + q3 = mRR_df.rolling(91, center=True, min_periods=1).quantile(0.75).signal.to_numpy() th2 = alpha * ((q3 - q1) / 2) mRR /= th2 # Subspace 2 ma = np.hstack( - [0, [np.max([dRR[i-1], dRR[i+1]]) for i in range(1, len(dRR)-1)], 0]) + [0, [np.max([dRR[i - 1], dRR[i + 1]]) for i in range(1, len(dRR) - 1)], 0] + ) mi = np.hstack( - [0, [np.min([dRR[i-1], dRR[i+1]]) for i in range(1, len(dRR)-1)], 0]) + [0, [np.min([dRR[i - 1], dRR[i + 1]]) for i in range(1, len(dRR) - 1)], 0] + ) s12 = ma s12[dRR < 0] = mi[dRR < 0] # Subspace 3 ma = np.hstack( - [[np.max([dRR[i+1], dRR[i+2]]) for i in range(0, len(dRR)-2)], 0, 0]) + [[np.max([dRR[i + 1], dRR[i + 2]]) for i in range(0, len(dRR) - 2)], 0, 0] + ) mi = np.hstack( - [[np.min([dRR[i+1], dRR[i+2]]) for i in range(0, len(dRR)-2)], 0, 0]) + [[np.min([dRR[i + 1], dRR[i + 2]]) for i in range(0, len(dRR) - 2)], 0, 0] + ) s22 = ma s22[dRR >= 0] = mi[dRR >= 0] @@ -324,32 +373,30 @@ def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2): ########## # Find ectobeats - cond1 = (s11 > 1) & (s12 < (-c1 * s11-c2)) - cond2 = (s11 < -1) & (s12 > (-c1 * s11+c2)) + cond1 = (s11 > 1) & (s12 < (-c1 * s11 - c2)) + cond2 = (s11 < -1) & (s12 > (-c1 * s11 + c2)) ectopic = cond1 | cond2 # No ectopic detection and correction at time serie edges ectopic[-2:] = False ectopic[:2] = False # Find long or shorts - longBeats = \ - ((s11 > 1) & (s22 < -1)) | ((np.abs(mRR) > 3) & (rr > np.median(rr))) - shortBeats = \ - ((s11 < -1) & (s22 > 1)) | ((np.abs(mRR) > 3) & (rr <= np.median(rr))) + longBeats = ((s11 > 1) & (s22 < -1)) | ((np.abs(mRR) > 3) & (rr > np.median(rr))) + shortBeats = ((s11 < -1) & (s22 > 1)) | ((np.abs(mRR) > 3) & (rr <= np.median(rr))) # Test if next interval is also outlier for cond in [longBeats, shortBeats]: - for i in range(len(cond)-2): + for i in range(len(cond) - 2): if cond[i] is True: - if np.abs(s11[i+1]) < np.abs(s11[i+2]): - cond[i+1] = True + if np.abs(s11[i + 1]) < np.abs(s11[i + 2]): + cond[i + 1] = True # Ectopic beats are not considered as short or long shortBeats[ectopic] = False longBeats[ectopic] = False # Missed vector - missed = np.abs((rr/2) - medRR) < th2 + missed = np.abs((rr / 2) - medRR) < th2 missed = missed & longBeats longBeats[missed] = False # Missed beats are not considered as long @@ -362,15 +409,26 @@ def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2): shortBeats[0], shortBeats[-1] = False, False longBeats[0], longBeats[-1] = False, False - artefacts = {'subspace1': s11, 'subspace2': s12, 'subspace3': s22, - 'mRR': mRR, 'ectopic': ectopic, 'long': longBeats, - 'short': shortBeats, 'missed': missed, 'extra': extra, - 'threshold1': th1, 'threshold2': th2} + artefacts = { + "subspace1": s11, + "subspace2": s12, + "subspace3": s22, + "mRR": mRR, + "ectopic": ectopic, + "long": longBeats, + "short": shortBeats, + "missed": missed, + "extra": extra, + "threshold1": th1, + "threshold2": th2, + } return artefacts -def interpolate_clipping(signal, threshold=255): +def interpolate_clipping( + signal: Union[List, np.ndarray], threshold: int = 255 +) -> np.ndarray: """Interoplate clipping artefacts. This function removes all data points equalling the provided threshold @@ -378,14 +436,14 @@ def interpolate_clipping(signal, threshold=255): Parameters ---------- - signal : 1d array-like + signal : np.ndarray or list Noisy signal. threshold : int Threshold of clipping artefact. Returns ------- - clean_signal : 1d array-like + clean_signal : np.ndarray Interpolated signal. Examples @@ -406,7 +464,7 @@ def interpolate_clipping(signal, threshold=255): Notes ----- Correct signal segment reaching recording threshold (default is 255) - using a cubic spline interpolation. Adapted from [#]_. + using a cubic spline interpolation. Adapted from [1]_. .. Warning:: If clipping artefact is found at the edge of the signal, this function will decrement the first/last value to allow interpolation, @@ -414,23 +472,25 @@ def interpolate_clipping(signal, threshold=255): References ---------- - .. [#] https://python-heart-rate-analysis-toolkit.readthedocs.io/en/latest/ + .. [1] https://python-heart-rate-analysis-toolkit.readthedocs.io/en/latest/ """ if isinstance(signal, list): signal = np.array(signal) # Security check for clipping at signal edge if signal[0] == threshold: - signal[0] = threshold-1 + signal[0] = threshold - 1 if signal[-1] == threshold: - signal[-1] = threshold-1 + signal[-1] = threshold - 1 time = np.arange(0, len(signal)) # Interpolate - f = interp1d(time[np.where(signal != 255)[0]], - signal[np.where(signal != 255)[0]], - kind='cubic') + f = interp1d( + time[np.where(signal != 255)[0]], + signal[np.where(signal != 255)[0]], + kind="cubic", + ) # Use the peaks vector as time input clean_signal = f(time) diff --git a/systole/hrv.py b/systole/hrv.py index ef45ad1c..01854d1b 100644 --- a/systole/hrv.py +++ b/systole/hrv.py @@ -1,17 +1,19 @@ # Author: Nicolas Legrand +from typing import Dict, List, Optional, Tuple, Union + import numpy as np import pandas as pd from scipy import interpolate from scipy.signal import welch -def nnX(x, t=50): +def nnX(x: Union[List, np.ndarray], t: int = 50) -> float: """Number of difference in successive R-R interval > t ms. Parameters ---------- - x : array like + x : np.ndarray or list 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. @@ -24,19 +26,19 @@ def nnX(x, t=50): if isinstance(x, list): x = np.asarray(x) if len(x.shape) > 1: - raise ValueError('X must be a 1darray') + raise ValueError("X must be a 1darray") # NN50: number of successive differences larger than t ms nn = np.sum(np.abs(np.diff(x)) > t) return nn -def pnnX(x, t=50): +def pnnX(x: Union[List, np.ndarray], t: int = 50) -> float: """Number of successive differences larger than a value (def = 50ms). Parameters ---------- - x : array like + x : np.ndarray or list 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. @@ -49,7 +51,7 @@ def pnnX(x, t=50): if isinstance(x, list): x = np.asarray(x) if len(x.shape) > 1: - raise ValueError('X must be a 1darray') + raise ValueError("X must be a 1darray") # nnX: number of successive differences larger than t ms nn = nnX(x, t) @@ -60,12 +62,12 @@ def pnnX(x, t=50): return pnnX -def rmssd(x): +def rmssd(x: Union[List, np.ndarray]) -> float: """Root Mean Square of Successive Differences. Parameters ---------- - x : array like + x : np.ndarray or list Interval time-series (R-R, beat-to-beat...), in miliseconds. Returns @@ -87,19 +89,19 @@ def rmssd(x): if isinstance(x, list): x = np.asarray(x) if len(x.shape) > 1: - raise ValueError('X must be a 1darray') + raise ValueError("X must be a 1darray") y = np.sqrt(np.mean(np.square(np.diff(x)))) return y -def time_domain(x): +def time_domain(x: Union[List, np.ndarray]) -> pd.DataFrame: """Extract all time domain parameters from R-R intervals. Parameters ---------- - x : 1d array-like + x : np.ndarray or list Interval time-series (R-R, beat-to-beat...), in miliseconds. Returns @@ -132,37 +134,36 @@ def time_domain(x): using the py:pandas.pivot_table() function: >>> pd.pivot_table(stats, values='Values', columns='Metric') """ - if isinstance(x, list): - x = np.asarray(x) + x = np.asarray(x) if len(x.shape) > 1: - raise ValueError('X must be a 1darray') + raise ValueError("X must be a 1darray") # Mean R-R intervals - mean_rr = round(np.mean(x)) + mean_rr = round(np.mean(x)) # type: ignore # Mean BPM - mean_bpm = round(np.mean(60000/x), 2) + mean_bpm = round(np.mean(60000 / x), 2) # type: ignore # Median BPM median_rr = round(np.median(x), 2) # Median BPM - median_bpm = round(np.median(60000/x), 2) + median_bpm = round(np.median(60000 / x), 2) # Minimum RR min_rr = round(np.min(x), 2) # Minimum BPM - min_bpm = round(np.min(60000/x), 2) + min_bpm = round(np.min(60000 / x), 2) # Maximum RR max_rr = round(np.max(x), 2) # Maximum BPM - max_bpm = round(np.max(60000/x), 2) + max_bpm = round(np.max(60000 / x), 2) # Standard deviation of R-R intervals - sdnn = round(x.std(ddof=1), 2) + sdnn = round(x.std(ddof=1), 2) # type: ignore # Root Mean Square of Successive Differences (RMSSD) rms = round(rmssd(x), 2) @@ -174,22 +175,51 @@ def time_domain(x): pnn = round(pnnX(x, t=50), 2) # Create summary dataframe - values = [mean_rr, mean_bpm, median_rr, median_bpm, min_rr, min_bpm, - max_rr, max_bpm, sdnn, rms, nn, pnn] - metrics = ['MeanRR', 'MeanBPM', 'MedianRR', 'MedianBPM', 'MinRR', 'MinBPM', - 'MaxRR', 'MaxBPM', 'SDNN', 'RMSSD', 'nn50', 'pnn50'] - - stats = pd.DataFrame({'Values': values, 'Metric': metrics}) + values = [ + mean_rr, + mean_bpm, + median_rr, + median_bpm, + min_rr, + min_bpm, + max_rr, + max_bpm, + sdnn, + rms, + nn, + pnn, + ] + metrics = [ + "MeanRR", + "MeanBPM", + "MedianRR", + "MedianBPM", + "MinRR", + "MinBPM", + "MaxRR", + "MaxBPM", + "SDNN", + "RMSSD", + "nn50", + "pnn50", + ] + + stats = pd.DataFrame({"Values": values, "Metric": metrics}) return stats -def frequency_domain(x, sfreq=5, method='welch', fbands=None): +def frequency_domain( + x: Union[List, np.ndarray], + sfreq: int = 5, + method: str = "welch", + fbands: Optional[Dict[str, Tuple[str, Tuple[float, float], str]]] = None, +) -> pd.DataFrame: """Extract the frequency domain features of heart rate variability. Parameters ---------- - x : list or 1d array-like + x : np.ndarray or list Interval time-series (R-R, beat-to-beat...), in miliseconds. sfreq : int The sampling frequency (Hz). @@ -198,9 +228,9 @@ def frequency_domain(x, sfreq=5, method='welch', fbands=None): 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 ------- @@ -227,11 +257,11 @@ def frequency_domain(x, sfreq=5, method='welch', fbands=None): """ # Interpolate R-R interval time = np.cumsum(x) - f = interpolate.interp1d(time, x, kind='cubic') - new_time = np.arange(time[0], time[-1], 1000/sfreq) # Sampling rate = 5 Hz + f = interpolate.interp1d(time, x, kind="cubic") + new_time = np.arange(time[0], time[-1], 1000 / sfreq) # sfreq = 5 Hz x = f(new_time) - if method == 'welch': + if method == "welch": # Define window length nperseg = 256 * sfreq @@ -241,57 +271,64 @@ def frequency_domain(x, sfreq=5, method='welch', fbands=None): # Compute Power Spectral Density freq, psd = welch(x=x, fs=sfreq, nperseg=nperseg, nfft=nperseg) - psd = psd/1000000 + psd = psd / 1000000 if fbands is None: - fbands = {'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']} + fbands = { + "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"), + } # Extract HRV parameters ######################## stats = pd.DataFrame([]) for band in fbands: - this_psd = psd[ - (freq >= fbands[band][1][0]) & (freq < fbands[band][1][1])] - this_freq = freq[ - (freq >= fbands[band][1][0]) & (freq < fbands[band][1][1])] + this_psd = psd[(freq >= fbands[band][1][0]) & (freq < fbands[band][1][1])] + this_freq = freq[(freq >= fbands[band][1][0]) & (freq < fbands[band][1][1])] # Peaks (Hz) peak = round(this_freq[np.argmax(this_psd)], 4) - stats = stats.append({'Values': peak, 'Metric': band+'_peak'}, - ignore_index=True) + stats = stats.append( + {"Values": peak, "Metric": band + "_peak"}, ignore_index=True + ) # Power (ms**2) power = np.trapz(x=this_freq, y=this_psd) * 1000000 - stats = stats.append({'Values': power, 'Metric': band+'_power'}, - ignore_index=True) + stats = stats.append( + {"Values": power, "Metric": band + "_power"}, ignore_index=True + ) - hf = stats.Values[stats.Metric == 'hf_power'].values[0] - lf = stats.Values[stats.Metric == 'lf_power'].values[0] - vlf = stats.Values[stats.Metric == 'vlf_power'].values[0] + hf = stats.Values[stats.Metric == "hf_power"].values[0] + lf = stats.Values[stats.Metric == "lf_power"].values[0] + vlf = stats.Values[stats.Metric == "vlf_power"].values[0] # Power (%) - power_per_vlf = vlf/(vlf+lf+hf)*100 - power_per_lf = lf/(vlf+lf+hf)*100 - power_per_hf = hf/(vlf+lf+hf)*100 + power_per_vlf = vlf / (vlf + lf + hf) * 100 + power_per_lf = lf / (vlf + lf + hf) * 100 + power_per_hf = hf / (vlf + lf + hf) * 100 # Power (n.u.) - power_nu_hf = hf/(hf + lf) - power_nu_lf = lf/(hf + lf) - - values = [power_per_vlf, power_per_lf, power_per_hf, - power_nu_hf, power_nu_lf] - metrics = ['power_vlf_per', 'power_lf_per', 'power_hf_per', - 'power_lf_nu', 'power_hf_nu'] - - stats = stats.append(pd.DataFrame({'Values': values, 'Metric': metrics}), - ignore_index=True) + power_nu_hf = hf / (hf + lf) + power_nu_lf = lf / (hf + lf) + + values = [power_per_vlf, power_per_lf, power_per_hf, power_nu_hf, power_nu_lf] + metrics = [ + "power_vlf_per", + "power_lf_per", + "power_hf_per", + "power_lf_nu", + "power_hf_nu", + ] + + stats = stats.append( + pd.DataFrame({"Values": values, "Metric": metrics}), ignore_index=True + ) return stats -def nonlinear(x): +def nonlinear(x: Union[List, np.ndarray]) -> pd.DataFrame: """Extract the non-linear features of heart rate variability. Parameters @@ -322,11 +359,10 @@ def nonlinear(x): diff_rr = np.diff(x) sd1 = np.sqrt(np.std(diff_rr, ddof=1) ** 2 * 0.5) - sd2 = np.sqrt(2 * np.std(x, ddof=1) ** 2 - 0.5 * np.std(diff_rr, - ddof=1) ** 2) + sd2 = np.sqrt(2 * np.std(x, ddof=1) ** 2 - 0.5 * np.std(diff_rr, ddof=1) ** 2) values = [sd1, sd2] - metrics = ['SD1', 'SD2'] + metrics = ["SD1", "SD2"] - stats = pd.DataFrame({'Values': values, 'Metric': metrics}) + stats = pd.DataFrame({"Values": values, "Metric": metrics}) return stats diff --git a/systole/plotly.py b/systole/plotly.py index d99565f9..13ddfc0f 100644 --- a/systole/plotly.py +++ b/systole/plotly.py @@ -1,88 +1,173 @@ # Author: Nicolas Legrand +from typing import TYPE_CHECKING, Dict, List, Union, overload + import numpy as np import pandas as pd -from systole.detection import oxi_peaks, ecg_peaks + from systole.correction import rr_artefacts -from systole.utils import heart_rate +from systole.detection import ecg_peaks, oxi_peaks +from systole.hrv import frequency_domain, nonlinear, time_domain from systole.plotting import plot_psd -from systole.hrv import time_domain, frequency_domain, nonlinear +from systole.utils import heart_rate + +if TYPE_CHECKING: + from plotly.graph_objs._figure import Figure -def plot_raw(signal, sfreq=75, type='ppg'): - """Interactive visualization of PPG signal and beats detection. +def plot_raw( + signal: Union[pd.DataFrame, List, np.ndarray], + sfreq: int = 75, + type: str = "ppg", + ecg_method: str = "hamilton", +) -> "Figure": + """Interactive visualization of PPG signal and systolic peaks 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. + signal : :py:class:`pandas.DataFrame`, :py:class:`numpy.ndarray` or list + Dataframe of PPG or ECG signal in the long format. If a data frame is + provided, it should contain at least one ``'time'`` and one colum for + signal(either ``'ppg'`` or ``'ecg'``). If an array is provided, it will + automatically create a DataFrame using the array as signal and + ``sfreq`` as sampling frequency. sfreq : int - Signal sampling frequency. Default is 75 Hz. + Signal sampling frequency. Default is set to 75 Hz. type : str - The recording modality. Can be ``'ppg'`` (pulse oximeter) or ``'ecg'`` - (electrocardiography). + The type of signal provided. Can be ``'ppg'`` (pulse oximeter) or + ``'ecg'`` (electrocardiography). The peak detection algorithm used + depend on the type of signal provided. + ecg_method : str + Peak detection algorithm used by the + :py:func:`systole.detection.ecg_peaks` function. Can be one of the + following: `'hamilton'`, `'christov'`, `'engelse-zeelenberg'`, + `'pan-tompkins'`, `'wavelet-transform'`, `'moving-average'`. The + default is `'hamilton'`. + + Returns + ------- + raw : :py:class:`plotly.graph_objects.Figure` + Instance of :py:class:`plotly.graph_objects.Figure`. + + See also + -------- + plot_events, plot_ectopic, plot_shortLong, plot_subspaces, plot_frequency, + plot_timedomain, plot_nonlinear + + Examples + -------- + + Plotting PPG recording. + + .. jupyter-execute:: + + from systole import import_ppg + from systole.plotly import plot_raw + # Import PPG recording as pandas data frame + ppg = import_ppg() + # Only use the first 60 seconds for demonstration + ppg = ppg[ppg.time<60] + plot_raw(ppg) + + Plotting ECG recording. + + .. jupyter-execute:: + + from systole import import_dataset1 + from systole.plotly import plot_raw + # Import PPG recording as pandas data frame + ecg = import_dataset1(modalities=['ECG']) + # Only use the first 60 seconds for demonstration + ecg = ecg[ecg.time<60] + plot_raw(ecg, type='ecg', sfreq=1000, ecg_method='pan-tompkins') """ 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': + 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) + elif type == "ecg": + signal, peaks = ecg_peaks(signal.ecg, method=ecg_method, find_local=True) else: - if type == 'ppg': + signal = np.asarray(signal) + 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 + elif type == "ecg": + signal, peaks = ecg_peaks( + signal, method=ecg_method, 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') + 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')) + ppg_trace = go.Scattergl( + x=time, + y=signal, + mode="lines", + name="PPG signal", + 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'))) + 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')) + 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'))) + 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 = make_subplots( + rows=2, + cols=1, + shared_xaxes=True, + vertical_spacing=0.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.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) @@ -92,6 +177,30 @@ def plot_raw(signal, sfreq=75, type='ppg'): return raw +@overload +def plot_ectopic( + rr: None, + artefacts: Dict[str, np.ndarray], +) -> "Figure": + ... + + +@overload +def plot_ectopic( + rr: Union[List[float], np.ndarray], + artefacts: None, +) -> "Figure": + ... + + +@overload +def plot_ectopic( + rr: Union[List[float], np.ndarray], + artefacts: Dict[str, np.ndarray], +) -> "Figure": + ... + + def plot_ectopic(rr=None, artefacts=None): """Plot interactive ectobeats subspace. @@ -105,29 +214,65 @@ def plot_ectopic(rr=None, artefacts=None): Returns ------- - subspacesPlot : plotly Figure - The interactive plot. + subspacesPlot : :py:class:`plotly.graph_objects.Figure` + Instance of :py:class:`plotly.graph_objects.Figure`. + + See also + -------- + plot_events, plot_ectopic, plot_shortLong, plot_subspaces, plot_frequency, + plot_timedomain, plot_nonlinear Notes ----- If both *rr* or *artefacts* are provided, will recompute *artefacts* given the current rr time-series. + + Examples + -------- + + Visualizing ectopic subspace from RR time series. + + .. jupyter-execute:: + + from systole import import_rr + from systole.plotly import plot_ectopic + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + plot_ectopic(rr) + + Visualizing ectopic subspace from the `artefact` dictionary. + + .. jupyter-execute:: + + from systole import import_rr + from systole.plotly import plot_ectopic + from systole.detection import rr_artefacts + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + # Use the rr_artefacts function to find ectopic beats + artefacts = rr_artefacts(rr) + plot_ectopic(artefacts=artefacts) """ - import plotly_express as px + import plotly.express as px import plotly.graph_objs as go c1, c2, xlim, ylim = 0.13, 0.17, 10, 5 if artefacts is None: if rr is None: - raise ValueError('rr or artefacts should be provided') + raise ValueError("rr or artefacts should be provided") artefacts = rr_artefacts(rr) - outliers = (artefacts['ectopic'] | artefacts['short'] | artefacts['long'] - | artefacts['extra'] | artefacts['missed']) + outliers = ( + artefacts["ectopic"] + | artefacts["short"] + | artefacts["long"] + | artefacts["extra"] + | artefacts["missed"] + ) # All vlaues fit in the x and y lims - for this_art in [artefacts['subspace1'], artefacts['subspace2']]: + for this_art in [artefacts["subspace1"], artefacts["subspace2"]]: this_art[this_art > xlim] = xlim this_art[this_art < -xlim] = -xlim this_art[this_art > ylim] = ylim @@ -136,94 +281,187 @@ def plot_ectopic(rr=None, artefacts=None): subspacesPlot = go.Figure() # Upper area - def f1(x): return -c1*x + c2 - subspacesPlot.add_trace(go.Scatter(x=[-10, -10, -1, -1], - y=[f1(-10), 10, 10, f1(-1)], - fill='toself', mode='lines', - opacity=0.2, showlegend=False, - fillcolor='gray', hoverinfo='none', - line_color='gray')) + def f1(x): + return -c1 * x + c2 + + subspacesPlot.add_trace( + go.Scatter( + x=[-10, -10, -1, -1], + y=[f1(-10), 10, 10, f1(-1)], + fill="toself", + mode="lines", + opacity=0.2, + showlegend=False, + fillcolor="gray", + hoverinfo="none", + line_color="gray", + ) + ) # Lower area - def f2(x): return -c1*x - c2 - subspacesPlot.add_trace(go.Scatter(x=[1, 1, 10, 10], - y=[f2(1), -10, -10, f2(10)], - fill='toself', mode='lines', opacity=0.2, - showlegend=False, fillcolor='gray', - hoverinfo='none', line_color='gray', - text="Points only")) + def f2(x): + return -c1 * x - c2 + + subspacesPlot.add_trace( + go.Scatter( + x=[1, 1, 10, 10], + y=[f2(1), -10, -10, f2(10)], + fill="toself", + mode="lines", + opacity=0.2, + showlegend=False, + fillcolor="gray", + hoverinfo="none", + line_color="gray", + text="Points only", + ) + ) # Plot normal intervals - subspacesPlot.add_trace(go.Scattergl(x=artefacts['subspace1'][~outliers], - y=artefacts['subspace2'][~outliers], - mode='markers', showlegend=False, - name='Normal', marker=dict(size=8, - color='#4c72b0', opacity=0.2, - line=dict(width=2, - color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][~outliers], + y=artefacts["subspace2"][~outliers], + mode="markers", + showlegend=False, + name="Normal", + marker=dict( + size=8, + color="#4c72b0", + opacity=0.2, + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) # Plot ectopic beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['ectopic']], - y=artefacts['subspace2'][artefacts['ectopic']], - mode='markers', name='Ectopic beats', - showlegend=False, marker=dict( - size=10, color='#c44e52', - line=dict(width=2, color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["ectopic"]], + y=artefacts["subspace2"][artefacts["ectopic"]], + mode="markers", + name="Ectopic beats", + showlegend=False, + marker=dict( + size=10, color="#c44e52", line=dict(width=2, color="DarkSlateGrey") + ), + ) + ) # Plot missed beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['missed']], - y=artefacts['subspace2'][artefacts['missed']], - mode='markers', name='Missed beats', - showlegend=False, marker=dict( - size=10, - color=px.colors.sequential.Greens[8], - line=dict(width=2, color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["missed"]], + y=artefacts["subspace2"][artefacts["missed"]], + mode="markers", + name="Missed beats", + showlegend=False, + marker=dict( + size=10, + color=px.colors.sequential.Greens[8], + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) # Plot long beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['long']], - y=artefacts['subspace2'][artefacts['long']], - mode='markers', name='Long beats', marker_symbol='square', - showlegend=False, marker=dict( - size=10, color=px.colors.sequential.Greens[6], - line=dict(width=2, color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["long"]], + y=artefacts["subspace2"][artefacts["long"]], + mode="markers", + name="Long beats", + marker_symbol="square", + showlegend=False, + marker=dict( + size=10, + color=px.colors.sequential.Greens[6], + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) # Plot extra beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['extra']], - y=artefacts['subspace2'][artefacts['extra']], - mode='markers', name='Extra beats', - showlegend=False, marker=dict(size=10, - color=px.colors.sequential.Purples[8], - line=dict(width=2, - color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["extra"]], + y=artefacts["subspace2"][artefacts["extra"]], + mode="markers", + name="Extra beats", + showlegend=False, + marker=dict( + size=10, + color=px.colors.sequential.Purples[8], + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) # Plot short beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['short']], - y=artefacts['subspace2'][artefacts['short']], - mode='markers', name='Short beats', marker_symbol='square', - showlegend=False, marker=dict(size=10, - color=px.colors.sequential.Purples[6], - line=dict(width=2, - color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["short"]], + y=artefacts["subspace2"][artefacts["short"]], + mode="markers", + name="Short beats", + marker_symbol="square", + showlegend=False, + marker=dict( + size=10, + color=px.colors.sequential.Purples[6], + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) subspacesPlot.update_layout( - width=600, height=600, xaxis_title="Subspace $S_{11}$", - yaxis_title="Subspace $S_{12}$", template='simple_white', - title={'text': "Ectopic beats", 'x': 0.5, 'xanchor': 'center', - 'yanchor': 'top'}) + width=600, + height=600, + xaxis_title=r"Subspace $S_{11}$", + yaxis_title=r"Subspace $S_{12}$", + template="simple_white", + title={ + "text": "Ectopic beats", + "x": 0.5, + "xanchor": "center", + "yanchor": "top", + }, + ) - subspacesPlot.update_xaxes(showline=True, linewidth=2, linecolor='black', - range=[-xlim, xlim]) - subspacesPlot.update_yaxes(showline=True, linewidth=2, linecolor='black', - range=[-ylim, ylim]) + subspacesPlot.update_xaxes( + showline=True, linewidth=2, linecolor="black", range=[-xlim, xlim] + ) + subspacesPlot.update_yaxes( + showline=True, linewidth=2, linecolor="black", range=[-ylim, ylim] + ) return subspacesPlot -def plot_shortLong(rr=None, artefacts=None): +@overload +def plot_shortLong( + rr: None, + artefacts: Dict[str, np.ndarray], +) -> "Figure": + ... + + +@overload +def plot_shortLong( + rr: Union[List[float], np.ndarray], + artefacts: None, +) -> "Figure": + ... + + +@overload +def plot_shortLong( + rr: Union[List[float], np.ndarray], + artefacts: Dict[str, np.ndarray], +) -> "Figure": + ... + + +def plot_shortLong(rr=None, artefacts=None) -> "Figure": """Plot interactive short/long subspace. Parameters @@ -236,29 +474,66 @@ def plot_shortLong(rr=None, artefacts=None): Returns ------- - subspacesPlot : plotly Figure - The interactive plot. + subspacesPlot : :py:class:`plotly.graph_objects.Figure` + Instance of :py:class:`plotly.graph_objects.Figure`. + + See also + -------- + plot_events, plot_ectopic, plot_shortLong, plot_subspaces, plot_frequency, + plot_timedomain, plot_nonlinear Notes ----- If both ``rr`` or ``artefacts`` are provided, will recompute ``artefacts`` given the current rr time-series. + + Examples + -------- + + Visualizing short/long and missed/extra intervals from RR time series. + + .. jupyter-execute:: + + from systole import import_rr + from systole.plotly import plot_shortLong + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + plot_shortLong(rr) + + Visualizing ectopic subspace from the `artefact` dictionary. + + .. jupyter-execute:: + + from systole import import_rr + from systole.plotly import plot_shortLong + from systole.detection import rr_artefacts + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + # Use the rr_artefacts function to short/long + # and extra/missed intervals + artefacts = rr_artefacts(rr) + plot_shortLong(artefacts=artefacts) """ - import plotly_express as px + import plotly.express as px import plotly.graph_objs as go xlim, ylim = 10, 10 if artefacts is None: if rr is None: - raise ValueError('rr or artefacts should be provided') + raise ValueError("rr or artefacts should be provided") artefacts = rr_artefacts(rr) - outliers = (artefacts['ectopic'] | artefacts['short'] | artefacts['long'] - | artefacts['extra'] | artefacts['missed']) + outliers = ( + artefacts["ectopic"] + | artefacts["short"] + | artefacts["long"] + | artefacts["extra"] + | artefacts["missed"] + ) # All vlaues fit in the x and y lims - for this_art in [artefacts['subspace1'], artefacts['subspace3']]: + for this_art in [artefacts["subspace1"], artefacts["subspace3"]]: this_art[this_art > xlim] = xlim this_art[this_art < -xlim] = -xlim this_art[this_art > ylim] = ylim @@ -267,103 +542,175 @@ def plot_shortLong(rr=None, artefacts=None): subspacesPlot = go.Figure() # Upper area - subspacesPlot.add_trace(go.Scatter(x=[-10, -10, -1, -1], - y=[1, 10, 10, 1], - fill='toself', mode='lines', - opacity=0.2, showlegend=False, - fillcolor='gray', hoverinfo='none', - line_color='gray')) + subspacesPlot.add_trace( + go.Scatter( + x=[-10, -10, -1, -1], + y=[1, 10, 10, 1], + fill="toself", + mode="lines", + opacity=0.2, + showlegend=False, + fillcolor="gray", + hoverinfo="none", + line_color="gray", + ) + ) # Lower area - subspacesPlot.add_trace(go.Scatter(x=[1, 1, 10, 10], - y=[-1, -10, -10, -1], - fill='toself', mode='lines', opacity=0.2, - showlegend=False, fillcolor='gray', - hoverinfo='none', line_color='gray', - text="Points only")) + subspacesPlot.add_trace( + go.Scatter( + x=[1, 1, 10, 10], + y=[-1, -10, -10, -1], + fill="toself", + mode="lines", + opacity=0.2, + showlegend=False, + fillcolor="gray", + hoverinfo="none", + line_color="gray", + text="Points only", + ) + ) # Plot normal intervals - subspacesPlot.add_trace(go.Scattergl(x=artefacts['subspace1'][~outliers], - y=artefacts['subspace3'][~outliers], - mode='markers', showlegend=False, - name='Normal', marker=dict(size=8, - color='#4c72b0', opacity=0.2, - line=dict(width=2, - color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][~outliers], + y=artefacts["subspace3"][~outliers], + mode="markers", + showlegend=False, + name="Normal", + marker=dict( + size=8, + color="#4c72b0", + opacity=0.2, + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) # Plot ectopic beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['ectopic']], - y=artefacts['subspace3'][artefacts['ectopic']], - mode='markers', name='Ectopic beats', - showlegend=False, marker=dict( - size=10, color='#c44e52', - line=dict(width=2, color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["ectopic"]], + y=artefacts["subspace3"][artefacts["ectopic"]], + mode="markers", + name="Ectopic beats", + showlegend=False, + marker=dict( + size=10, color="#c44e52", line=dict(width=2, color="DarkSlateGrey") + ), + ) + ) # Plot missed beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['missed']], - y=artefacts['subspace3'][artefacts['missed']], - mode='markers', name='Missed beats', - showlegend=False, marker=dict( - size=10, - color=px.colors.sequential.Greens[8], - line=dict(width=2, color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["missed"]], + y=artefacts["subspace3"][artefacts["missed"]], + mode="markers", + name="Missed beats", + showlegend=False, + marker=dict( + size=10, + color=px.colors.sequential.Greens[8], + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) # Plot long beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['long']], - y=artefacts['subspace3'][artefacts['long']], - mode='markers', name='Long beats', marker_symbol='square', - showlegend=False, marker=dict( - size=10, color=px.colors.sequential.Greens[6], - line=dict(width=2, color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["long"]], + y=artefacts["subspace3"][artefacts["long"]], + mode="markers", + name="Long beats", + marker_symbol="square", + showlegend=False, + marker=dict( + size=10, + color=px.colors.sequential.Greens[6], + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) # Plot extra beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['extra']], - y=artefacts['subspace3'][artefacts['extra']], - mode='markers', name='Extra beats', - showlegend=False, marker=dict(size=10, - color=px.colors.sequential.Purples[8], - line=dict(width=2, - color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["extra"]], + y=artefacts["subspace3"][artefacts["extra"]], + mode="markers", + name="Extra beats", + showlegend=False, + marker=dict( + size=10, + color=px.colors.sequential.Purples[8], + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) # Plot short beats - subspacesPlot.add_trace(go.Scattergl( - x=artefacts['subspace1'][artefacts['short']], - y=artefacts['subspace3'][artefacts['short']], - mode='markers', name='Short beats', marker_symbol='square', - showlegend=False, marker=dict(size=10, - color=px.colors.sequential.Purples[6], - line=dict(width=2, - color='DarkSlateGrey')))) + subspacesPlot.add_trace( + go.Scattergl( + x=artefacts["subspace1"][artefacts["short"]], + y=artefacts["subspace3"][artefacts["short"]], + mode="markers", + name="Short beats", + marker_symbol="square", + showlegend=False, + marker=dict( + size=10, + color=px.colors.sequential.Purples[6], + line=dict(width=2, color="DarkSlateGrey"), + ), + ) + ) subspacesPlot.update_layout( - width=600, height=600, xaxis_title="Subspace $S_{11}$", - yaxis_title="Subspace $S_{12}$", template='simple_white', - title={'text': "Short/longs beats", 'x': 0.5, 'xanchor': 'center', - 'yanchor': 'top'}) + width=600, + height=600, + xaxis_title=r"Subspace $S_{11}$", + yaxis_title=r"Subspace $S_{12}$", + template="simple_white", + title={ + "text": "Short/longs beats", + "x": 0.5, + "xanchor": "center", + "yanchor": "top", + }, + ) - subspacesPlot.update_xaxes(showline=True, linewidth=2, linecolor='black', - range=[-xlim, xlim]) - subspacesPlot.update_yaxes(showline=True, linewidth=2, linecolor='black', - range=[-ylim, ylim]) + subspacesPlot.update_xaxes( + showline=True, linewidth=2, linecolor="black", range=[-xlim, xlim] + ) + subspacesPlot.update_yaxes( + showline=True, linewidth=2, linecolor="black", range=[-ylim, ylim] + ) return subspacesPlot -def plot_subspaces(rr): +def plot_subspaces(rr: Union[List[float], np.ndarray], height: float = 400) -> "Figure": """Plot hrv subspace as described by Lipponen & Tarvainen (2019) [#]_. Parameters ---------- - rr : 1d array-like + rr : np.ndarray or list Interval time-series (R-R, beat-to-beat...), in miliseconds. + height : int + Height of the figure. The width will be set to `height*2` by default. Returns ------- - fig : `go.Figure` - The figure. + fig : :py:class:`plotly.graph_objects.Figure` + Instance of :py:class:`plotly.graph_objects.Figure`. + + See also + -------- + plot_events, plot_ectopic, plot_shortLong, plot_subspaces, plot_frequency, + plot_timedomain, plot_nonlinear References ---------- @@ -371,15 +718,34 @@ def plot_subspaces(rr): 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 + + Examples + -------- + + Visualizing artefacts from RR time series. + + .. jupyter-execute:: + + from systole import import_rr + from systole.plotly import plot_subspaces + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + plot_subspaces(rr) """ from plotly.subplots import make_subplots + rr = np.asarray(rr) + xlim, ylim = 10, 10 - fig = make_subplots(rows=1, cols=2, column_widths=[0.5, 0.5], - subplot_titles=("Ectopic", "Short/longs beats")) + fig = make_subplots( + rows=1, + cols=2, + column_widths=[0.5, 0.5], + subplot_titles=("Ectopic", "Short/longs beats"), + ) - ectopic = plot_ectopic(rr.copy()) - sl = plot_shortLong(rr.copy()) + ectopic = plot_ectopic(rr=rr) # type: ignore + sl = plot_shortLong(rr=rr) # type: ignore for traces in ectopic.data: fig.add_traces([traces], rows=[1], cols=[1]) @@ -387,24 +753,33 @@ def plot_subspaces(rr): fig.add_traces([traces], rows=[1], cols=[2]) fig.update_layout( - width=1200, height=600, xaxis_title="Subspace $S_{11}$", - yaxis_title="Subspace $S_{12}$", xaxis2_title="Subspace $S_{21}$", - yaxis2_title="Subspace $S_{22}$", template='simple_white') + width=height * 2, + height=height, + xaxis_title=r"Subspace $S_{11}$", + yaxis_title=r"Subspace $S_{12}$", + xaxis2_title=r"Subspace $S_{21}$", + yaxis2_title=r"Subspace $S_{22}$", + template="simple_white", + ) - fig.update_xaxes(showline=True, linewidth=2, linecolor='black', - range=[-xlim, xlim], row=1, col=1) - fig.update_yaxes(showline=True, linewidth=2, linecolor='black', - range=[-ylim, ylim], row=1, col=1) + fig.update_xaxes( + showline=True, linewidth=2, linecolor="black", range=[-xlim, xlim], row=1, col=1 + ) + fig.update_yaxes( + showline=True, linewidth=2, linecolor="black", range=[-ylim, ylim], row=1, col=1 + ) - fig.update_xaxes(showline=True, linewidth=2, linecolor='black', - range=[-xlim, xlim], row=1, col=2) - fig.update_yaxes(showline=True, linewidth=2, linecolor='black', - range=[-ylim, ylim], row=1, col=2) + fig.update_xaxes( + showline=True, linewidth=2, linecolor="black", range=[-xlim, xlim], row=1, col=2 + ) + fig.update_yaxes( + showline=True, linewidth=2, linecolor="black", range=[-ylim, ylim], row=1, col=2 + ) return fig -def plot_frequency(rr): +def plot_frequency(rr: Union[np.ndarray, list]) -> "Figure": """Plot PSD and frequency domain metrics. Parameters @@ -414,8 +789,26 @@ def plot_frequency(rr): Returns ------- - fig : `go.Figure` - The figure. + fig : :py:class:`plotly.graph_objects.Figure` + Instance of :py:class:`plotly.graph_objects.Figure`. + + See also + -------- + plot_events, plot_ectopic, plot_shortLong, plot_subspaces, plot_frequency, + plot_timedomain, plot_nonlinear + + Examples + -------- + + Visualizing HRV frequency domain from RR time series. + + .. jupyter-execute:: + + from systole import import_rr + from systole.plotly import plot_frequency + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + plot_frequency(rr) """ import plotly.graph_objs as go from plotly.subplots import make_subplots @@ -423,68 +816,101 @@ def plot_frequency(rr): df = frequency_domain(rr).round(2) fig = make_subplots( - rows=2, cols=1, + rows=2, + cols=1, shared_xaxes=True, - specs=[[{"type": "scatter"}], - [{"type": "table"}]], - ) - - fig.add_trace(go.Table( - header=dict( - values=['Frequency band (HZ)', 'Peak (Hz)', - 'Power (ms2)', - 'Power (%)', 'Power (n.u.)'], align='center' - ), - cells=dict( - values=[['VLF \n (0-0.04 Hz)', 'LF \n (0.04 - 0.15 Hz)', - 'HF \n (0.15 - 0.4 Hz)'], - [df[df.Metric == 'vlf_peak'].Values, - df[df.Metric == 'lf_peak'].Values, - df[df.Metric == 'hf_peak'].Values], - [df[df.Metric == 'vlf_power'].Values, - df[df.Metric == 'lf_power'].Values, - df[df.Metric == 'hf_power'].Values], - ['-', - df[df.Metric == 'power_lf_nu'].Values, - df[df.Metric == 'power_hf_nu'].Values], - ['-', - df[df.Metric == 'power_lf_per'].Values, - df[df.Metric == 'power_hf_per'].Values], - ], align='center')), row=2, col=1) + specs=[[{"type": "scatter"}], [{"type": "table"}]], + ) + + fig.add_trace( + go.Table( + header=dict( + values=[ + "Frequency band (HZ)", + "Peak (Hz)", + "Power (ms2)", + "Power (%)", + "Power (n.u.)", + ], + align="center", + ), + cells=dict( + values=[ + [ + "VLF \n (0-0.04 Hz)", + "LF \n (0.04 - 0.15 Hz)", + "HF \n (0.15 - 0.4 Hz)", + ], + [ + df[df.Metric == "vlf_peak"].Values, + df[df.Metric == "lf_peak"].Values, + df[df.Metric == "hf_peak"].Values, + ], + [ + df[df.Metric == "vlf_power"].Values, + df[df.Metric == "lf_power"].Values, + df[df.Metric == "hf_power"].Values, + ], + [ + "-", + df[df.Metric == "power_lf_nu"].Values, + df[df.Metric == "power_hf_nu"].Values, + ], + [ + "-", + df[df.Metric == "power_lf_per"].Values, + df[df.Metric == "power_hf_per"].Values, + ], + ], + align="center", + ), + ), + row=2, + col=1, + ) freq, psd = plot_psd(rr, show=False) - fbands = {'vlf': ['Very low frequency', (0.003, 0.04), '#4c72b0'], - 'lf': ['Low frequency', (0.04, 0.15), '#55a868'], - 'hf': ['High frequency', (0.15, 0.4), '#c44e52']} + fbands = { + "vlf": ("Very low frequency", (0.003, 0.04), "#4c72b0"), + "lf": ("Low frequency", (0.04, 0.15), "#55a868"), + "hf": ("High frequency", (0.15, 0.4), "#c44e52"), + } - for f in ['vlf', 'lf', 'hf']: + for f in ["vlf", "lf", "hf"]: mask = (freq >= fbands[f][1][0]) & (freq <= fbands[f][1][1]) - fig.add_trace(go.Scatter( - x=freq[mask], - y=psd[mask], - fill='tozeroy', - mode='lines', - showlegend=False, - line_color=fbands[f][2], - line=dict(shape="spline", - smoothing=1, - width=1, - color="#fac1b7")), row=1, col=1) + fig.add_trace( + go.Scatter( + x=freq[mask], + y=psd[mask], + fill="tozeroy", + mode="lines", + showlegend=False, + line_color=fbands[f][2], + line=dict(shape="spline", smoothing=1, width=1, color="#fac1b7"), + ), + row=1, + col=1, + ) fig.update_layout( - plot_bgcolor="white", paper_bgcolor="white", - margin=dict(l=5, r=5, b=5, t=5), autosize=True, width=600, height=600, - xaxis_title='Frequencies (Hz)', yaxis_title='PSD', - title={'text': "FFT Spectrum", 'x': 0.5, 'xanchor': 'center', - 'yanchor': 'top'}) - fig.update_xaxes(showline=True, linewidth=2, linecolor='black') - fig.update_yaxes(showline=True, linewidth=2, linecolor='black') + plot_bgcolor="white", + paper_bgcolor="white", + margin=dict(l=5, r=5, b=5, t=5), + autosize=True, + width=600, + height=600, + xaxis_title="Frequencies (Hz)", + yaxis_title="PSD", + title={"text": "FFT Spectrum", "x": 0.5, "xanchor": "center", "yanchor": "top"}, + ) + fig.update_xaxes(showline=True, linewidth=2, linecolor="black") + fig.update_yaxes(showline=True, linewidth=2, linecolor="black") return fig -def plot_nonlinear(rr): +def plot_nonlinear(rr: Union[np.ndarray, List]) -> "Figure": """Plot nonlinear domain. Parameters @@ -494,71 +920,131 @@ def plot_nonlinear(rr): Returns ------- - fig : `go.Figure` - The figure. + fig : :py:class:`plotly.graph_objects.Figure` + Instance of :py:class:`plotly.graph_objects.Figure`. + + See also + -------- + plot_events, plot_ectopic, plot_shortLong, plot_subspaces, plot_frequency, + plot_timedomain, plot_nonlinear + + Examples + -------- + + Visualizing HRV non linear domain from RR time series. + + .. jupyter-execute:: + + from systole import import_rr + from systole.plotly import plot_nonlinear + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + plot_nonlinear(rr) """ import plotly.graph_objs as go from plotly.subplots import make_subplots + rr = np.asarray(rr) + df = nonlinear(rr).round(2) fig = make_subplots( - rows=2, cols=1, - specs=[[{"type": "scatter"}], - [{"type": "table"}]], - ) - - fig.add_trace(go.Table( - header=dict( - values=['Pointcare Plot', 'Value'], align='center' - ), - cells=dict( - values=[['SD1', 'SD2'], - [df[df.Metric == 'SD1'].Values, - df[df.Metric == 'SD2'].Values]], align='center')), - row=2, col=1) - - ax_min = rr.min() - (rr.max() - rr.min())*.1 - ax_max = rr.max() + (rr.max() - rr.min())*.1 - - fig.add_trace(go.Scattergl( - x=rr[:-1], - y=rr[1:], - mode='markers', - opacity=0.6, - showlegend=False, - marker=dict(size=8, - color='#4c72b0', - line=dict(width=2, color='DarkSlateGrey')))) + rows=2, + cols=1, + specs=[[{"type": "scatter"}], [{"type": "table"}]], + ) + + fig.add_trace( + go.Table( + header=dict( + values=["Pointcare Plot", "Value"], align="center" + ), + cells=dict( + values=[ + ["SD1", "SD2"], + [df[df.Metric == "SD1"].Values, df[df.Metric == "SD2"].Values], + ], + align="center", + ), + ), + row=2, + col=1, + ) + + ax_min = rr.min() - (rr.max() - rr.min()) * 0.1 + ax_max = rr.max() + (rr.max() - rr.min()) * 0.1 + + fig.add_trace( + go.Scattergl( + x=rr[:-1], + y=rr[1:], + mode="markers", + opacity=0.6, + showlegend=False, + marker=dict( + size=8, color="#4c72b0", line=dict(width=2, color="DarkSlateGrey") + ), + ) + ) fig.add_trace(go.Scatter(x=[0, 4000], y=[0, 4000], showlegend=False)) fig.update_layout( - plot_bgcolor="white", paper_bgcolor="white", - margin=dict(l=5, r=5, b=5, t=5), autosize=True, width=500, height=800, - xaxis_title='RRn (ms)', yaxis_title='RRn+1 (ms)', - title={'text': "Pointcare Plot", 'x': 0.5, - 'xanchor': 'center', 'yanchor': 'top'}) - fig.update_xaxes(showline=True, linewidth=2, linecolor='black', - range=[ax_min, ax_max]) - fig.update_yaxes(showline=True, linewidth=2, linecolor='black', - range=[ax_min, ax_max]) + plot_bgcolor="white", + paper_bgcolor="white", + margin=dict(l=5, r=5, b=5, t=5), + autosize=True, + width=500, + height=800, + xaxis_title="RRn (ms)", + yaxis_title="RRn+1 (ms)", + title={ + "text": "Pointcare Plot", + "x": 0.5, + "xanchor": "center", + "yanchor": "top", + }, + ) + fig.update_xaxes( + showline=True, linewidth=2, linecolor="black", range=[ax_min, ax_max] + ) + fig.update_yaxes( + showline=True, linewidth=2, linecolor="black", range=[ax_min, ax_max] + ) return fig -def plot_timedomain(rr): - """Plot time domain. +def plot_timedomain(rr: Union[np.ndarray, list]) -> "Figure": + """Plot time domain metrics and the histogram of RR intervals. Parameters ---------- - rr : 1d array-like + rr : np.ndarray or list Interval time-series (R-R, beat-to-beat...), in miliseconds. Returns ------- - fig : `go.Figure` - The figure. + fig : :py:class:`plotly.graph_objects.Figure` + Instance of :py:class:`plotly.graph_objects.Figure`. + + See also + -------- + plot_events, plot_ectopic, plot_shortLong, plot_subspaces, plot_frequency, + plot_timedomain, plot_nonlinear + + Examples + -------- + + Visualizing HRV time domain metrics from RR time series. + + .. jupyter-execute:: + + from systole import import_rr + from systole.plotly import plot_nonlinear + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + plot_nonlinear(rr) """ import plotly.graph_objs as go from plotly.subplots import make_subplots @@ -566,37 +1052,50 @@ def plot_timedomain(rr): df = time_domain(rr).round(2) fig = make_subplots( - rows=2, cols=1, - specs=[[{"type": "scatter"}], - [{"type": "table"}]], - ) - - fig.add_trace(go.Table( - header=dict( - values=['Variable', 'Unit', 'Value'], - align='center' - ), - cells=dict( - values=[['Mean RR', 'Mean BPM', 'SDNN', 'RMSSD', 'pnn50'], - ['(ms)', '(1/min)', '(ms)', '(ms)', '(%)'], - [df[df.Metric == 'MeanRR'].Values, - df[df.Metric == 'MeanBPM'].Values, - df[df.Metric == 'SDNN'].Values, - df[df.Metric == 'RMSSD'].Values, - df[df.Metric == 'pnn50'].Values], - ], align='center')), - row=2, col=1) - - fig.add_trace(go.Histogram(x=rr, marker={'line': {'width': 2}, - 'color': '#4c72b0'})) + rows=2, + cols=1, + specs=[[{"type": "scatter"}], [{"type": "table"}]], + ) + + fig.add_trace( + go.Table( + header=dict( + values=["Variable", "Unit", "Value"], + align="center", + ), + cells=dict( + values=[ + ["Mean RR", "Mean BPM", "SDNN", "RMSSD", "pnn50"], + ["(ms)", "(1/min)", "(ms)", "(ms)", "(%)"], + [ + df[df.Metric == "MeanRR"].Values, + df[df.Metric == "MeanBPM"].Values, + df[df.Metric == "SDNN"].Values, + df[df.Metric == "RMSSD"].Values, + df[df.Metric == "pnn50"].Values, + ], + ], + align="center", + ), + ), + row=2, + col=1, + ) + + fig.add_trace(go.Histogram(x=rr, marker={"line": {"width": 2}, "color": "#4c72b0"})) fig.update_layout( - plot_bgcolor="white", paper_bgcolor="white", - margin=dict(l=5, r=5, b=5, t=5), autosize=True, width=500, height=800, - xaxis_title='RR intervals (ms)', yaxis_title='Counts', - title={'text': "Distribution", 'x': 0.5, - 'xanchor': 'center', 'yanchor': 'top'}) - fig.update_xaxes(showline=True, linewidth=2, linecolor='black') - fig.update_yaxes(showline=True, linewidth=2, linecolor='black') + plot_bgcolor="white", + paper_bgcolor="white", + margin=dict(l=5, r=5, b=5, t=5), + autosize=True, + width=500, + height=800, + xaxis_title="RR intervals (ms)", + yaxis_title="Counts", + title={"text": "Distribution", "x": 0.5, "xanchor": "center", "yanchor": "top"}, + ) + fig.update_xaxes(showline=True, linewidth=2, linecolor="black") + fig.update_yaxes(showline=True, linewidth=2, linecolor="black") return fig diff --git a/systole/plotting.py b/systole/plotting.py index dae085b3..96bbd29d 100644 --- a/systole/plotting.py +++ b/systole/plotting.py @@ -1,90 +1,175 @@ # Author: Nicolas Legrand import itertools +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union + +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt import seaborn as sns -from systole.detection import rr_artefacts, oxi_peaks -from systole.utils import heart_rate +from matplotlib.axes import Axes from scipy.interpolate import interp1d from scipy.signal import welch +from systole.detection import ecg_peaks, oxi_peaks, rr_artefacts +from systole.utils import heart_rate + +if TYPE_CHECKING: + from systole.recording import Oximeter -def plot_hr(x, sfreq=75, outliers=None, unit='rr', kind='cubic', ax=None): - """Plot the instantaneous heart rate time course. + +def plot_raw( + signal: Union[pd.DataFrame, np.ndarray], + sfreq: int = 75, + type: str = "ppg", + ecg_method: str = "hamilton", + ax: Optional[Axes] = None, + figsize: Tuple[float, float] = (13, 5), + **kwargs +) -> Axes: + """Visualization of PPG signal and systolic peaks detection. Parameters ---------- - x : 1d array-like or `systole.recording.Oximeter` - The recording instance, where additional channels track different - events using boolean recording. If a 1d array is provided, should be - a peaks vector. + signal : :py:class:`pandas.DataFrame` or :py:class:`numpy.ndarray` + Dataframe of PPG or ECG signal in the long format. If a data frame is + provided, it should contain at least one ``'time'`` and one colum for + signal(either ``'ppg'`` or ``'ecg'``). If an array is provided, it will + automatically create a DataFrame using the array as signal and + ``sfreq`` as sampling frequency. sfreq : int - Signal sampling frequency. Default is 75 Hz. - outliers : 1d array-like - Boolean array indexing RR intervals considered as outliers and plotted - separately. - unit : str - The heartrate unit in use. Can be 'rr' (R-R intervals, in ms) - or 'bpm' (beats per minutes). Default is 'rr'. - kind : str - The method to use (parameter of `scipy.interpolate.interp1d`). - ax : `Matplotlib.Axes` or None - Where to draw the plot. Default is *None* (create a new figure). + Signal sampling frequency. Default is set to 75 Hz. + type : str + The type of signal provided. Can be ``'ppg'`` (pulse oximeter) or + ``'ecg'`` (electrocardiography). The peak detection algorithm used + depend on the type of signal provided. + ecg_method : str + Peak detection algorithm used by the + :py:func:`systole.detection.ecg_peaks` function. Can be one of the + following: `'hamilton'`, `'christov'`, `'engelse-zeelenberg'`, + `'pan-tompkins'`, `'wavelet-transform'`, `'moving-average'`. The + default is `'hamilton'`. + figsize : tuple + Figure size. Default set to `(13, 5)`. + **kwargs : keyword arguments + Additional arguments will be passed to + `:py:func:systole.detection.oxi_peaks()` or + `:py:func:systole.detection.ecg_peaks()`, depending on the type + of data. Returns ------- - ax : `Matplotlib.Axes` - The figure. - """ - if isinstance(x, list): - x = np.asarray(x) - if isinstance(x, np.ndarray): - # If a RR time serie is provided, transform to peaks vector - if not ((x == 0) | (x == 1)).all(): - x = np.round(x).astype(int) - peaks = np.zeros(np.cumsum(x)[-1]) - peaks = np.insert(peaks, 0, 1) - peaks[np.cumsum(x)] = 1 - sfreq = 1000 - else: - peaks = x - else: # Oximeter instance - peaks = np.asarray(x.peaks) + ax : :class:`matplotlib.axes.Axes` + The matplotlib axes containing the plot. - # Compute the interpolated instantaneous heart rate - hr, times = heart_rate(peaks, sfreq=sfreq, unit=unit, kind=kind) + See also + -------- + plot_events, plot_subspaces, plot_events, plot_psd, plot_oximeter - # New peaks vector - f = interp1d(np.arange(0, len(peaks)/sfreq, 1/sfreq), peaks, - kind='linear', bounds_error=False, - fill_value=(np.nan, np.nan)) - new_peaks = f(times) + Examples + -------- + Plotting PPG recording. - if ax is None: - fig, ax = plt.subplots(figsize=(13, 5)) + .. plot:: - # Interpolate instantaneous HR - ax.plot(times, hr, linestyle='--', color='gray') + >>> from systole import import_ppg + >>> from systole.plotting import plot_raw + >>> # Import PPG recording as pandas data frame + >>> ppg = import_ppg() + >>> # Only use the first 60 seconds for demonstration + >>> ppg = ppg[ppg.time<60] + >>> plot_raw(ppg) - # Heart beats - ax.plot(times[np.where(new_peaks)[0]], hr[np.where(new_peaks)[0]], 'bo', - alpha=0.5) + Plotting ECG recording. - # Show outliers - if outliers is not None: - idx = np.where(peaks)[0][1:][np.where(outliers)[0]] - ax.plot(times[idx], hr[idx], 'ro') + .. plot:: - ax.set_xlabel('Time (s)') - ax.set_ylabel('R-R (ms)') - ax.set_title('Instantaneous Heart rate', fontweight='bold') + >>> from systole import import_dataset1 + >>> from systole.plotting import plot_raw + >>> # Import PPG recording as pandas data frame + >>> ecg = import_dataset1(modalities=['ECG']) + >>> # Only use the first 60 seconds for demonstration + >>> ecg = ecg[ecg.time<60] + >>> plot_raw(ecg, type='ecg', sfreq=1000, ecg_method='pan-tompkins') + """ + if isinstance(signal, pd.DataFrame): + # Find peaks - Remove learning phase + if type == "ppg": + signal, peaks = oxi_peaks(signal.ppg, noise_removal=False, **kwargs) + elif type == "ecg": + signal, peaks = ecg_peaks( + signal.ecg, method=ecg_method, find_local=True, **kwargs + ) + else: + if type == "ppg": + signal, peaks = oxi_peaks( + signal, noise_removal=False, sfreq=sfreq, **kwargs + ) + elif type == "ecg": + signal, peaks = ecg_peaks( + signal, method=ecg_method, sfreq=sfreq, find_local=True, **kwargs + ) + time = np.arange(0, len(signal)) / 1000 + + # Extract heart rate + hr, time = heart_rate(peaks, sfreq=1000, unit="rr", kind="linear") + + ############# + # Upper panel + ############# + if ax is None: + fig, ax = plt.subplots(ncols=1, nrows=2, figsize=figsize, sharex=True) + + # Signal + ax[0].plot(time, signal, label="PPG signal", linewidth=1, color="#c44e52") + + # Peaks + ax[0].scatter( + x=time[peaks], + y=signal[peaks], + marker="o", + label="Peaks", + s=30, + color="white", + edgecolors="DarkSlateGrey", + ) + if type == "ppg": + ax[0].set_title("PPG recording") + ax[0].set_ylabel("PPG level (a.u.)") + elif type == "ecg": + ax[0].set_title("ECG recording") + ax[0].set_ylabel("ECG (mV)") + ax[0].grid(True) + + ############# + # Lower panel + ############# + + # Instantaneous Heart Rate - Lines + ax[1].plot(time, hr, label="R-R intervals", linewidth=1, color="#4c72b0") + + # Instantaneous Heart Rate - Peaks + ax[1].scatter( + x=time[peaks], + y=hr[peaks], + marker="o", + label="R-R intervals", + s=20, + color="white", + edgecolors="DarkSlateGrey", + ) + ax[1].set_title("Instantaneous heart rate") + ax[1].set_xlabel("Time (s)") + ax[1].set_ylabel("R-R interval (ms)") + ax[1].grid(True) + + plt.tight_layout() + sns.despine() return ax -def plot_events(oximeter, ax=None): +def plot_events(oximeter: "Oximeter", ax: Optional[Axes] = None) -> Axes: """Plot events occurence across recording. Parameters @@ -92,38 +177,47 @@ def plot_events(oximeter, ax=None): oximeter : `systole.recording.Oximeter` The recording instance, where additional channels track different events using boolean recording. - ax : `Matplotlib.Axes` or None + ax : :class:`matplotlib.axes.Axes` or None Where to draw the plot. Default is *None* (create a new figure). Returns ------- - ax : `Matplotlib.Axes` - The figure. + ax : :class:`matplotlib.axes.Axes` + The matplotlib axes containing the plot. """ if ax is None: fig, ax = plt.subplots(figsize=(13, 5)) - palette = itertools.cycle(sns.color_palette('deep')) - events = oximeter.channels.copy() + palette = itertools.cycle(sns.color_palette("deep")) + if oximeter.channels is not None: + events = oximeter.channels.copy() + else: + raise ValueError("No event found") for i, ch in enumerate(events): - ax.fill_between(x=oximeter.times, y1=i, y2=i+0.5, - color=next(palette), - where=np.array(events[ch]) == 1) + ax.fill_between( + x=oximeter.times, + y1=i, + y2=i + 0.5, + color=next(palette), + where=np.array(events[ch]) == 1, + ) # Add y ticks with channels names ax.set_yticks(np.arange(len(events)) + 0.5) ax.set_yticklabels([key for key in events]) - ax.set_xlabel('Time (s)') - ax.set_title('Events', fontweight='bold') + ax.set_xlabel("Time (s)") + ax.set_title("Events", fontweight="bold") return ax -def plot_oximeter(x, sfreq=75, ax=None): +def plot_oximeter( + x: "Union[Oximeter, np.ndarray, List]", sfreq: int = 75, ax: Optional[Axes] = None +) -> Axes: """Plot PPG signal. Parameters ---------- - x : 1d array-like or `systole.recording.Oximeter` + x : :py:class:`numpy.ndarray`, list 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. @@ -132,55 +226,61 @@ def plot_oximeter(x, sfreq=75, ax=None): Returns ------- - ax : `Matplotlib.Axes` - The figure. + ax : :class:`matplotlib.axes.Axes` + The matplotlib axes containing the plot. """ if isinstance(x, (list, np.ndarray)): - times = np.arange(0, len(x)/sfreq, 1/sfreq) + 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' + 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' + label = "Online estimation" if ax is None: fig, ax = plt.subplots(figsize=(13, 5)) - ax.set_title('Oximeter recording', fontweight='bold') + 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.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 plot_subspaces(x, c1=.17, c2=.13, xlim=10, ylim=5, ax=None): +def plot_subspaces( + rr: Union[List, np.ndarray], + c1: float = 0.17, + c2: float = 0.13, + xlim: float = 10.0, + ylim: float = 5.0, + ax: Optional[Axes] = None, + figsize: Tuple[float, float] = (10, 5), +) -> Axes: """Plot hrv subspace as described by Lipponen & Tarvainen (2019). Parameters ---------- - x : 1d array-like + rr : :py:class:`numpy.ndarray` or list Array of RR intervals or subspace1. If subspace1 is provided, subspace2 and 3 must also be provided. c1 : float @@ -189,17 +289,19 @@ def plot_subspaces(x, c1=.17, c2=.13, xlim=10, ylim=5, ax=None): c2 : float Fixed variable controling the intersect of the threshold lines. Default is 0.17. - xlim : int + xlim : float Absolute range of the x axis. Default is 10. - ylim : int + ylim : float Absolute range of the y axis. Default is 5. - ax : `Matplotlib.Axes` or None + ax : :class:`matplotlib.axes.Axes` or None Where to draw the plot. Default is *None* (create a new figure). + figsize : tuple + Figure size. Default set to `(10, 5)` Returns ------- - ax : `Matplotlib.Axes` - The figure. + ax : :class:`matplotlib.axes.Axes` + The matplotlib axes containing the plot. References ---------- @@ -207,79 +309,133 @@ def plot_subspaces(x, c1=.17, c2=.13, xlim=10, ylim=5, ax=None): 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 + + Examples + -------- + + Visualizing artefacts from RR time series. + + .. plot:: + + from systole import import_rr + from systole.plotting import plot_subspaces + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + plot_subspaces(rr) + """ - if not isinstance(x, (np.ndarray, np.generic)): - x = np.asarray(x) - artefacts = rr_artefacts(x) + if not isinstance(rr, (np.ndarray, np.generic)): + rr = np.asarray(rr) + artefacts = rr_artefacts(rr) # Rescale to show outlier in scatterplot if xlim is not None: - artefacts['subspace1'][artefacts['subspace1'] < -xlim] = -xlim - artefacts['subspace1'][artefacts['subspace1'] > xlim] = xlim + artefacts["subspace1"][artefacts["subspace1"] < -xlim] = -xlim + artefacts["subspace1"][artefacts["subspace1"] > xlim] = xlim if ylim is not None: - artefacts['subspace2'][artefacts['subspace2'] < -ylim] = -ylim - artefacts['subspace2'][artefacts['subspace2'] > ylim] = ylim + artefacts["subspace2"][artefacts["subspace2"] < -ylim] = -ylim + artefacts["subspace2"][artefacts["subspace2"] > ylim] = ylim - artefacts['subspace3'][artefacts['subspace3'] < -ylim*2] = -ylim*2 - artefacts['subspace3'][artefacts['subspace3'] > ylim*2] = ylim*2 + artefacts["subspace3"][artefacts["subspace3"] < -ylim * 2] = -ylim * 2 + artefacts["subspace3"][artefacts["subspace3"] > ylim * 2] = ylim * 2 # Filter for normal beats - normalBeats = ((~artefacts['ectopic']) & (~artefacts['short']) & - (~artefacts['long']) & (~artefacts['missed']) & - (~artefacts['extra'])) + normalBeats = ( + (~artefacts["ectopic"]) + & (~artefacts["short"]) + & (~artefacts["long"]) + & (~artefacts["missed"]) + & (~artefacts["extra"]) + ) ############# # First panel ############# if ax is None: - fig, ax = plt.subplots(1, 2, figsize=(10, 5)) + fig, ax = plt.subplots(1, 2, figsize=figsize) # Plot normal beats - ax[0].scatter(artefacts['subspace1'][normalBeats], - artefacts['subspace2'][normalBeats], - color='gray', edgecolors='k', s=15, - alpha=0.2, zorder=10, label='Normal') + ax[0].scatter( + artefacts["subspace1"][normalBeats], + artefacts["subspace2"][normalBeats], + color="gray", + edgecolors="k", + s=15, + alpha=0.2, + zorder=10, + label="Normal", + ) # Plot outliers - ax[0].scatter(artefacts['subspace1'][artefacts['ectopic']], - artefacts['subspace2'][artefacts['ectopic']], - color='r', edgecolors='k', zorder=10, label='Ectopic') - ax[0].scatter(artefacts['subspace1'][artefacts['short']], - artefacts['subspace2'][artefacts['short']], - color='b', edgecolors='k', zorder=10, - marker='s', label='Short') - ax[0].scatter(artefacts['subspace1'][artefacts['long']], - artefacts['subspace2'][artefacts['long']], - color='g', edgecolors='k', zorder=10, - marker='s', label='Long') - ax[0].scatter(artefacts['subspace1'][artefacts['missed']], - artefacts['subspace2'][artefacts['missed']], - color='g', edgecolors='k', zorder=10, label='Missed') - ax[0].scatter(artefacts['subspace1'][artefacts['extra']], - artefacts['subspace2'][artefacts['extra']], - color='b', edgecolors='k', zorder=10, label='Extra') + ax[0].scatter( + artefacts["subspace1"][artefacts["ectopic"]], + artefacts["subspace2"][artefacts["ectopic"]], + color="r", + edgecolors="k", + zorder=10, + label="Ectopic", + ) + ax[0].scatter( + artefacts["subspace1"][artefacts["short"]], + artefacts["subspace2"][artefacts["short"]], + color="b", + edgecolors="k", + zorder=10, + marker="s", + label="Short", + ) + ax[0].scatter( + artefacts["subspace1"][artefacts["long"]], + artefacts["subspace2"][artefacts["long"]], + color="g", + edgecolors="k", + zorder=10, + marker="s", + label="Long", + ) + ax[0].scatter( + artefacts["subspace1"][artefacts["missed"]], + artefacts["subspace2"][artefacts["missed"]], + color="g", + edgecolors="k", + zorder=10, + label="Missed", + ) + ax[0].scatter( + artefacts["subspace1"][artefacts["extra"]], + artefacts["subspace2"][artefacts["extra"]], + color="b", + edgecolors="k", + zorder=10, + label="Extra", + ) + # Upper area - def f1(x): return -c1*x + c2 - ax[0].plot([-1, -10], [f1(-1), f1(-10)], 'k', linewidth=1, linestyle='--') - ax[0].plot([-1, -1], [f1(-1), 10], 'k', linewidth=1, linestyle='--') + def f1(x): + return -c1 * x + c2 + + ax[0].plot([-1, -10], [f1(-1), f1(-10)], "k", linewidth=1, linestyle="--") + ax[0].plot([-1, -1], [f1(-1), 10], "k", linewidth=1, linestyle="--") x = [-10, -10, -1, -1] y = [f1(-10), 10, 10, f1(-1)] - ax[0].fill(x, y, color='gray', alpha=0.3) + ax[0].fill(x, y, color="gray", alpha=0.3) # Lower area - def f2(x): return -c1*x - c2 - ax[0].plot([1, 10], [f2(1), f2(10)], 'k', linewidth=1, linestyle='--') - ax[0].plot([1, 1], [f2(1), -10], 'k', linewidth=1, linestyle='--') + def f2(x): + return -c1 * x - c2 + + ax[0].plot([1, 10], [f2(1), f2(10)], "k", linewidth=1, linestyle="--") + ax[0].plot([1, 1], [f2(1), -10], "k", linewidth=1, linestyle="--") x = [1, 1, 10, 10] y = [f2(1), -10, -10, f2(10)] - ax[0].fill(x, y, color='gray', alpha=0.3) + ax[0].fill(x, y, color="gray", alpha=0.3) - ax[0].set_xlabel('Subspace $S_{11}$') - ax[0].set_ylabel('Subspace $S_{12}$') + ax[0].set_xlabel("Subspace $S_{11}$") + ax[0].set_ylabel("Subspace $S_{12}$") ax[0].set_ylim(-ylim, ylim) ax[0].set_xlim(-xlim, xlim) - ax[0].set_title('Subspace 1 \n (ectopic beats detection)') + ax[0].set_title("Subspace 1 \n (ectopic beats detection)") ax[0].legend() ############## @@ -287,48 +443,79 @@ def f2(x): return -c1*x - c2 ############## # Plot normal beats - ax[1].scatter(artefacts['subspace1'][normalBeats], - artefacts['subspace3'][normalBeats], - color='gray', edgecolors='k', alpha=0.2, - zorder=10, s=15, label='Normal') + ax[1].scatter( + artefacts["subspace1"][normalBeats], + artefacts["subspace3"][normalBeats], + color="gray", + edgecolors="k", + alpha=0.2, + zorder=10, + s=15, + label="Normal", + ) # Plot outliers - ax[1].scatter(artefacts['subspace1'][artefacts['ectopic']], - artefacts['subspace3'][artefacts['ectopic']], - color='r', edgecolors='k', zorder=10, label='Ectopic') - ax[1].scatter(artefacts['subspace1'][artefacts['short']], - artefacts['subspace3'][artefacts['short']], - color='b', edgecolors='k', zorder=10, - marker='s', label='Short') - ax[1].scatter(artefacts['subspace1'][artefacts['long']], - artefacts['subspace3'][artefacts['long']], - color='g', edgecolors='k', zorder=10, - marker='s', label='Long') - ax[1].scatter(artefacts['subspace1'][artefacts['missed']], - artefacts['subspace3'][artefacts['missed']], - color='g', edgecolors='k', zorder=10, label='Missed') - ax[1].scatter(artefacts['subspace1'][artefacts['extra']], - artefacts['subspace3'][artefacts['extra']], - color='b', edgecolors='k', zorder=10, label='Extra') + ax[1].scatter( + artefacts["subspace1"][artefacts["ectopic"]], + artefacts["subspace3"][artefacts["ectopic"]], + color="r", + edgecolors="k", + zorder=10, + label="Ectopic", + ) + ax[1].scatter( + artefacts["subspace1"][artefacts["short"]], + artefacts["subspace3"][artefacts["short"]], + color="b", + edgecolors="k", + zorder=10, + marker="s", + label="Short", + ) + ax[1].scatter( + artefacts["subspace1"][artefacts["long"]], + artefacts["subspace3"][artefacts["long"]], + color="g", + edgecolors="k", + zorder=10, + marker="s", + label="Long", + ) + ax[1].scatter( + artefacts["subspace1"][artefacts["missed"]], + artefacts["subspace3"][artefacts["missed"]], + color="g", + edgecolors="k", + zorder=10, + label="Missed", + ) + ax[1].scatter( + artefacts["subspace1"][artefacts["extra"]], + artefacts["subspace3"][artefacts["extra"]], + color="b", + edgecolors="k", + zorder=10, + label="Extra", + ) # Upper area - ax[1].plot([-1, -10], [1, 1], 'k', linewidth=1, linestyle='--') - ax[1].plot([-1, -1], [1, 10], 'k', linewidth=1, linestyle='--') + ax[1].plot([-1, -10], [1, 1], "k", linewidth=1, linestyle="--") + ax[1].plot([-1, -1], [1, 10], "k", linewidth=1, linestyle="--") x = [-10, -10, -1, -1] y = [1, 10, 10, 1] - ax[1].fill(x, y, color='gray', alpha=0.3) + ax[1].fill(x, y, color="gray", alpha=0.3) # Lower area - ax[1].plot([1, 10], [-1, -1], 'k', linewidth=1, linestyle='--') - ax[1].plot([1, 1], [-1, -10], 'k', linewidth=1, linestyle='--') + ax[1].plot([1, 10], [-1, -1], "k", linewidth=1, linestyle="--") + ax[1].plot([1, 1], [-1, -10], "k", linewidth=1, linestyle="--") x = [1, 1, 10, 10] y = [-1, -10, -10, -1] - ax[1].fill(x, y, color='gray', alpha=0.3) + ax[1].fill(x, y, color="gray", alpha=0.3) - ax[1].set_xlabel('Subspace $S_{21}$') - ax[1].set_ylabel('Subspace $S_{22}$') - ax[1].set_ylim(-ylim*2, ylim*2) + ax[1].set_xlabel("Subspace $S_{21}$") + ax[1].set_ylabel("Subspace $S_{22}$") + ax[1].set_ylim(-ylim * 2, ylim * 2) ax[1].set_xlim(-xlim, xlim) - ax[1].set_title('Subspace 2 \n (long and short beats detection)') + ax[1].set_title("Subspace 2 \n (long and short beats detection)") ax[1].legend() plt.tight_layout() @@ -336,42 +523,62 @@ def f2(x): return -c1*x - c2 return ax -def plot_psd(x, sfreq=5, method='welch', fbands=None, low=0.003, - high=0.4, show=True, ax=None): - """Plot PSD of heart rate variability. +def plot_psd( + x: Union[List, np.ndarray], + sfreq: int = 5, + method: str = "welch", + fbands: Optional[Dict[str, Tuple[str, Tuple[float, float], str]]] = None, + show: bool = True, + ax: Optional[Axes] = None, +) -> Union[Tuple[np.ndarray, np.ndarray], Axes]: + """Plot frequency component of the heart rate variability. + + Parameters + ---------- + x : :py:class:`numpy.ndarray` or list + Length of R-R intervals (default is in miliseconds). + sfreq : int + The sampling frequency. + method : str + The method used to extract freauency power. Default set to `'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')} + show : bool + Plot the power spectrum density. Default is *True*. + ax : :class:`matplotlib.axes.Axes` or None + Where to draw the plot. Default is `None` (create a new figure). + + Returns + ------- + ax or (freq, psd) : :class:`matplotlib.axes.Axes` or tuple of numpy array + If show is `*`True*, return the PSD plot. If show is *False*, will + return the frequencies and PSD level as arrays. - Parameters - ---------- - x : 1d array-like - Length of R-R intervals (default is in miliseconds). - sfreq : int - The sampling frequency. - method : str - The method used to extract freauency power. Default set to `'welch'`. - fbands : None or 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']} - show : bool - Plot the power spectrum density. Default is *True*. - ax : `Matplotlib.Axes` or None - Where to draw the plot. Default is *None* (create a new figure). + Examples + -------- + + Visualizing artefacts from RR time series. + + .. plot:: + + from systole import import_rr + from systole.plotting import plot_psd + # Import PPG recording as numpy array + rr = import_rr().rr.to_numpy() + plot_psd(rr) - Returns - ------- - ax or (freq, psd) : `Matplotlib.Axes` or 1d array-like - If show is *True*, return the PSD plot. If show is *False*, will return - the frequencies and PSD level as arrays. """ # Interpolate R-R interval time = np.cumsum(x) - f = interp1d(time, x, kind='cubic') - new_time = np.arange(time[0], time[-1], 1000/sfreq) # Sampling rate = 5 Hz + f = interp1d(time, x, kind="cubic") + new_time = np.arange(time[0], time[-1], 1000 / sfreq) # sfreq = 5 Hz x = f(new_time) - if method == 'welch': + if method == "welch": # Define window length nperseg = 256 * sfreq @@ -379,44 +586,52 @@ def plot_psd(x, sfreq=5, method='welch', fbands=None, low=0.003, nperseg = len(x) # Compute Power Spectral Density - freq, psd = welch(x=x, fs=sfreq, nperseg=nperseg, nfft=nperseg*10) + freq, psd = welch(x=x, fs=sfreq, nperseg=nperseg, nfft=nperseg * 10) - psd = psd/1000000 + psd = psd / 1000000 if fbands is None: - fbands = {'vlf': ['Very low frequency', (0.003, 0.04), '#4c72b0'], - 'lf': ['Low frequency', (0.04, 0.15), '#55a868'], - 'hf': ['High frequency', (0.15, 0.4), '#c44e52']} + fbands = { + "vlf": ("Very low frequency", (0.003, 0.04), "#4c72b0"), + "lf": ("Low frequency", (0.04, 0.15), "#55a868"), + "hf": ("High frequency", (0.15, 0.4), "#c44e52"), + } if show is True: # Plot the PSD if ax is None: fig, ax = plt.subplots(figsize=(8, 5)) - ax.plot(freq, psd, 'k') - for f in ['vlf', 'lf', 'hf']: + ax.plot(freq, psd, "k") + for f in ["vlf", "lf", "hf"]: mask = (freq >= fbands[f][1][0]) & (freq <= fbands[f][1][1]) - ax.fill_between(freq, psd, where=mask, color=fbands[f][2], - alpha=0.8) - ax.axvline(x=fbands[f][1][0], - linestyle='--', - color='gray') + ax.fill_between(freq, psd, where=mask, color=fbands[f][2], alpha=0.8) + ax.axvline(x=fbands[f][1][0], linestyle="--", color="gray") ax.set_xlim(0.003, 0.4) - ax.set_xlabel('Frequency [Hz]') - ax.set_ylabel('PSD [$s^2$/Hz]') - ax.set_title('Power Spectral Density', fontweight='bold') + ax.set_xlabel("Frequency [Hz]") + ax.set_ylabel("PSD [$s^2$/Hz]") + ax.set_title("Power Spectral Density", fontweight="bold") return ax else: return freq, psd -def circular(data, bins=32, density='area', offset=0, mean=False, norm=True, - units='radians', color=None, ax=None): +def circular( + data: Union[List, np.ndarray], + bins: int = 32, + density: str = "area", + offset: float = 0.0, + mean: bool = False, + norm: bool = True, + units: str = "radians", + color: Optional[str] = None, + ax: Optional[Axes] = None, +) -> Axes: """Plot polar histogram. Parameters ---------- - data : array-like or list + data : :py:class:`numpy.ndarray` or list Angular values, in radians. bins : int Use even value to have a bin edge at zero. @@ -427,21 +642,21 @@ def circular(data, bins=32, density='area', offset=0, mean=False, norm=True, Where 0 will be placed on the circle, in radians. Default set to 0 (right). mean : bool - If True, show the mean and 95% CI. Default set to `False` + If `True`, show the mean and 95% CI. Default set to `False` norm : boolean Normalize the distribution between 0 and 1. units : str - Unit of the angular representation. Can be 'degree' or 'radian'. + Unit of the angular representation. Can be `'degree'` or `'radian'`. Default set to 'radians'. - color : Matplotlib color + color : str The color of the bars. - ax : `Matplotlib.Axes` or None - Where to draw the plot. Default is *None* (create a new figure). + ax : :class:`matplotlib.axes.Axes` or None + Where to draw the plot. Default is `None` (create a new figure). Returns ------- - ax : `Matplotlib.Axes` - The figure. + ax : :class:`matplotlib.axes.Axes` + The matplotlib axes containing the plot. Notes ----- @@ -469,46 +684,55 @@ def circular(data, bins=32, density='area', offset=0, mean=False, norm=True, .. [#] https://jwalton.info/Matplotlib-rose-plots/ .. [#] https://pingouin-stats.org/_modules/pingouin/circular.html#circ_mean + """ if isinstance(data, list): data = np.asarray(data) if color is None: - color = '#539dcc' + color = "#539dcc" if ax is None: ax = plt.subplot(111, polar=True) # Bin data and count - count, bin = np.histogram(data, bins=bins, range=(0, np.pi*2)) + count, bin = np.histogram(data, bins=bins, range=(0, np.pi * 2)) # Compute width widths = np.diff(bin)[0] - if density == 'area': # Default + if density == "area": # Default # Area to assign each bin area = count / data.size # Calculate corresponding bin radius - radius = (area / np.pi)**.5 + radius = (area / np.pi) ** 0.5 alpha = (count * 0) + 1 - elif density == 'height': # Using height (can be misleading) + elif density == "height": # Using height (can be misleading) radius = count / data.size alpha = (count * 0) + 1 - elif density == 'alpha': # Using transparency + elif density == "alpha": # Using transparency radius = (count * 0) + 1 # Alpha level to each bin alpha = count / data.size alpha = alpha / alpha.max() else: - raise ValueError('Invalid method') + raise ValueError("Invalid method") if norm is True: radius = radius / radius.max() # Plot data on ax for b, r, a in zip(bin[:-1], radius, alpha): - plt.bar(b, r, align='edge', width=widths, - edgecolor='k', linewidth=1, color=color, alpha=a) + ax.bar( + b, + r, + align="edge", + width=widths, + edgecolor="k", + linewidth=1, + color=color, + alpha=a, + ) # Plot mean and CI if mean: @@ -516,7 +740,7 @@ def circular(data, bins=32, density='area', offset=0, mean=False, norm=True, alpha = np.array(data) w = np.ones_like(alpha) circ_mean = np.angle(np.multiply(w, np.exp(1j * alpha)).sum(axis=0)) - ax.plot(circ_mean, radius.max(), 'ko') + ax.plot(circ_mean, radius.max(), "ko") # Set the direction of the zero angle ax.set_theta_offset(offset) @@ -525,20 +749,33 @@ def circular(data, bins=32, density='area', offset=0, mean=False, norm=True, ax.set_yticks([]) if units == "radians": - label = ['$0$', r'$\pi/4$', r'$\pi/2$', r'$3\pi/4$', - r'$\pi$', r'$5\pi/4$', r'$3\pi/2$', r'$7\pi/4$'] + label = [ + "$0$", + r"$\pi/4$", + r"$\pi/2$", + r"$3\pi/4$", + r"$\pi$", + r"$5\pi/4$", + r"$3\pi/2$", + r"$7\pi/4$", + ] ax.set_xticklabels(label) plt.tight_layout() return ax -def plot_circular(data, y=None, hue=None, **kwargs): +def plot_circular( + data: pd.DataFrame, + y: Union[str, List, None] = None, + hue: Optional[Union[str, List[str]]] = None, + **kwargs +) -> Axes: """Plot polar histogram. Parameters ---------- - data : `pd.DataFrame` + data : :py:class:`pandas.DataFrame` Angular data (rad.). y : str or list If data is a pandas instance, column containing the angular values. @@ -548,8 +785,8 @@ def plot_circular(data, y=None, hue=None, **kwargs): Returns ------- - ax : `Matplotlib.Axes` - The figure. + ax : :class:`matplotlib.axes.Axes` + The matplotlib axes containing the plot. Examples -------- @@ -562,12 +799,13 @@ def plot_circular(data, y=None, hue=None, **kwargs): y = np.random.uniform(0, np.pi*2, 100) data = pd.DataFrame(data={'x': x, 'y': y}).melt() plot_circular(data=data, y='value', hue='variable') + """ # Check data format if isinstance(data, pd.DataFrame): - assert data.shape[0] > 0, 'Data must have at least 1 row.' + assert data.shape[0] > 0, "Data must have at least 1 row." - palette = itertools.cycle(sns.color_palette('deep')) + palette = itertools.cycle(sns.color_palette("deep")) if hue is None: ax = circular(data[y].values, **kwargs) @@ -575,7 +813,7 @@ def plot_circular(data, y=None, hue=None, **kwargs): else: n_plot = data[hue].nunique() - fig, axs = plt.subplots(1, n_plot, subplot_kw=dict(projection='polar')) + fig, axs = plt.subplots(1, n_plot, subplot_kw=dict(projection="polar")) for i, cond in enumerate(data[hue].unique()): diff --git a/systole/recording.py b/systole/recording.py index d4138c10..5cc52798 100644 --- a/systole/recording.py +++ b/systole/recording.py @@ -1,16 +1,20 @@ # Author: Nicolas Legrand -import numpy as np -import time import socket +import time +from struct import unpack +from typing import Dict, List, Optional + +import numpy as np +import pandas as pd import serial from serial.tools import list_ports -from struct import unpack + from systole.detection import oxi_peaks -from systole.plotting import plot_oximeter, plot_events, plot_hr +from systole.plotting import plot_events, plot_oximeter, plot_raw -class Oximeter(): +class Oximeter: """Recording PPG signal with Nonin pulse oximeter. Parameters @@ -22,6 +26,11 @@ class Oximeter(): add_channels : int If int, will create as many additionnal channels. If None, no additional channels created. + data_format : str + Data format returned by the USB dongle ("2" or "7"). See + https://www.nonin.com/wp-content/uploads/6000-7000-CP-7602-000-11_ENG.pdf + for details. The pulse waveform value is automatically normalized and + range between 0 and 255 both for data format "2" and "7". Attributes ---------- @@ -110,38 +119,85 @@ class Oximeter(): of in waiting samples. We recommend storing regularly 5 minutes recording as .npy file using the :py:func:save() function. """ - def __init__(self, serial, sfreq=75, add_channels=None): + def __init__( + self, + serial, + sfreq: int = 75, + add_channels: Optional[int] = None, + data_format: str = "2", + ): + self.reset(serial, sfreq, add_channels, data_format) + + def reset( + self, + serial, + sfreq: int = 75, + add_channels: Optional[int] = None, + data_format: str = "2", + ): + """Initialize/restart the recording instance. + + Parameters + ---------- + serial : pySerial object + The `serial` instance interfacing with the USB port. + sfreq : int + The sampling frequency of the recording. Defautl is 75 Hz. + add_channels : int + If int, will create as many additionnal channels. If None, no + additional channels created. + data_format : str + Data format returned by the USB dongle ("2" or "7"). See + https://www.nonin.com/wp-content/uploads/6000-7000-CP-7602-000-11_ENG.pdf + for details. The pulse waveform value is automatically normalized and + range between 0 and 255 both for data format "2" and "7". + + Returns + ------- + Oximeter instance. + """ self.serial = serial self.lag = 0 self.sfreq = sfreq self.dist = int(self.sfreq * 0.2) # Initialize recording with empty lists - self.instant_rr = [] - self.recording = [] - self.times = [] - self.n_channels = add_channels - self.threshold = [] - self.diff = [] - self.peaks = [] + self.instant_rr: List[float] = [] + self.recording: List[float] = [] + self.times: List[float] = [] + self.n_channels: Optional[int] = add_channels + self.threshold: List[float] = [] + self.diff: List[float] = [] + self.peaks: List[int] = [] if add_channels is not None: - self.channels = {} + self.channels: Optional[Dict[str, List]] = {} for i in range(add_channels): - self.channels['Channel_' + str(i)] = [] + self.channels[f"Channel_{i}"] = [] else: self.channels = None - def add_paquet(self, paquet, window=1): + # Set the get value function depending on the data format + self.data_format = data_format + if data_format == "2": + self.get_value = self.data_format2 + elif data_format == "7": + self.get_value = self.data_format7 + else: + raise ValueError('Data format should be "2" or "7"') + + return self + + def add_paquet(self, value: int, window: float = 1.0): """Read a portion of data. Parameters ---------- - paquet : int - The data to record. Should be an integer between 0 and 240. - window : int or float + value : int + The data to record. Should be an integer between 0 and 255. + window : float Length of the window used to compute threshold (seconds). Default - is `1`. + is `1.0`. Returns ------- @@ -154,7 +210,7 @@ def add_paquet(self, paquet, window=1): """ # Store new data - self.recording.append(paquet) + self.recording.append(value) self.peaks.append(0) # Add 0 to the additional channels @@ -166,12 +222,13 @@ def add_paquet(self, paquet, window=1): if not self.times: self.times = [0] else: - self.times.append(len(self.times)/self.sfreq) + self.times.append(len(self.times) / self.sfreq) # Update threshold window = int(window * self.sfreq) - self.threshold.append((np.mean(self.recording[-window:]) + - np.std(self.recording[-window:]))) + self.threshold.append( + (np.mean(self.recording[-window:]) + np.std(self.recording[-window:])) + ) # Store new differential if not exist if not self.diff: @@ -180,7 +237,7 @@ def add_paquet(self, paquet, window=1): self.diff.append(self.recording[-1] - self.recording[-2]) # Is it a threshold crossing value? - if paquet > self.threshold[-1]: + if value > self.threshold[-1]: # Is the new differential zero or crossing zero? if (self.diff[-1] <= 0) & (self.diff[-2] > 0): @@ -192,13 +249,14 @@ def add_paquet(self, paquet, window=1): # Update instantaneous heart rate if sum(self.peaks) >= 2: self.instant_rr.append( - (np.diff(np.where(self.peaks)[0])[-1]/self.sfreq)*1000) + (np.diff(np.where(self.peaks)[0])[-1] / self.sfreq) * 1000 + ) else: - self.instant_rr.append(float('nan')) + self.instant_rr.append(float("nan")) return self - def check(self, paquet): + def check(self, paquet: list): """Check if the provided paquet is correct Parameters @@ -208,7 +266,7 @@ def check(self, paquet): """ check = False if len(paquet) >= 5: - if paquet[0] == 1: + if (paquet[0] == 1) | (paquet[0] >= 128): if (paquet[1] >= 0) & (paquet[1] <= 255): if (paquet[2] >= 0) & (paquet[2] <= 255): if paquet[3] <= 127: @@ -217,6 +275,26 @@ def check(self, paquet): return check + def data_format2(self, paquet): + """Extract pulse waveform value for data format 2. + + Parameters + ---------- + paquet : list + A list containg 5 items. + """ + return paquet[2] + + def data_format7(self, paquet): + """Extract pulse waveform value for data format 7. + + Parameters + ---------- + paquet : list + A list containg 5 items. + """ + return ((paquet[1] * 256 + paquet[2]) / 65535) * 255 + def find_peaks(self, **kwargs): """Find peaks in recorded signal. @@ -227,17 +305,16 @@ def find_peaks(self, **kwargs): Other Parameters ---------------- - **kwargs : `~systole.detection.oxi_peaks` properties. + **kwargs : py:func:`systole.detection.oxi_peaks` properties. """ # Peak detection - resampled_signal, peaks = oxi_peaks(self.recording, - new_sfreq=75, **kwargs) + 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 + self.rr = (np.diff(np.where(peaks)[0]) / self.sfreq) * 1000 # Beats per minutes - self.bpm = 60000/self.rr + self.bpm = 60000 / self.rr return self @@ -253,7 +330,7 @@ def plot_events(self, ax=None): return ax - def plot_hr(self, ax=None): + def plot_raw(self, ax=None): """Plot heartrate extracted from PPG recording. Returns @@ -261,7 +338,7 @@ def plot_hr(self, ax=None): fig, ax : Matplotlib instances. The figure and axe instances. """ - ax = plot_hr(self, ax=ax) + ax = plot_raw(self.recording, ax=ax) return ax @@ -277,7 +354,7 @@ def plot_recording(self, ax=None): return ax - def read(self, duration): + def read(self, duration: float): """Read PPG signal for some amount of time. Parameters @@ -291,12 +368,12 @@ def read(self, duration): # Store Oxi level paquet = list(self.serial.read(5)) if self.check(paquet): - self.add_paquet(paquet[2]) + self.add_paquet(value=self.get_value(paquet)) else: self.setup() return self - def readInWaiting(self, stop=False): + def readInWaiting(self, stop: bool = False): """Read in wainting oxi data. Parameters @@ -309,47 +386,84 @@ def readInWaiting(self, stop=False): # Store Oxi level paquet = list(self.serial.read(5)) if self.check(paquet): - self.add_paquet(paquet[2]) + self.add_paquet(value=self.get_value(paquet)) else: if stop is True: - raise ValueError('Synch error') + raise ValueError("Synch error") else: - print('Synch error') + print("Synch error") while True: self.serial.reset_input_buffer() paquet = list(self.serial.read(5)) if self.check(paquet=paquet): break - def save(self, fname): + def save(self, fname: str): """Save the recording instance. Parameters ---------- fname : str - The file name. + The file name. The file extension can be `.npy` for + :class:`numpy.array` or `.txt` for :class:`pandas.DataFrame`. If + no extension is provided, will use the `.npy` extension by default. + + Notes + ----- + If the signal is saved as a :class:`pandas.DataFrame`, the resulting data + frame will contain the following columns: + * `signal` + * `peaks` + * `instant_rr` + * `time` + If stim channels are provided, additional columns are appended as Channel_`i`, + for `i` additional channels. + + If the signal is saved as a :class:`numpy.array`, the first dimension + will encode the channels in that order, and the second dimension the + samples. """ + # Sanity checks if len(self.peaks) != len(self.recording): - self.peak = np.zeros(len(self.recording)) + self.peak = [0 * len(self.recording)] if len(self.instant_rr) != len(self.recording): - self.instant_rr = np.zeros(len(self.recording)) + self.instant_rr = [0 * len(self.recording)] if len(self.times) != len(self.recording): - self.times = np.zeros(len(self.recording)) + self.times = [0 * len(self.recording)] - if len(self.threshold) != len(self.recording): - self.threshold = np.zeros(len(self.recording)) + # Data that should be saved + saveList = [ + np.asarray(self.recording), + np.asarray(self.peaks), + np.asarray(self.instant_rr), + np.asarray(self.times), + ] - recording = np.array([np.asarray(self.recording), - np.asarray(self.peaks), - np.asarray(self.instant_rr), - np.asarray(self.times), - np.asarray(self.threshold)]) - - np.save(fname, recording) + # Add stim channels if provided + if self.channels is not None: + for i in range(len(self.channels)): + if len(self.channels[f"Channel_{i}"]) != len(self.recording): + self.channels["Channel_" + str(i)] = [0 * len(self.recording)] + saveList.append(np.asarray(self.channels[f"Channel_{i}"])) + + # Check data format and save + if fname.endswith(".txt"): + colnames = ["signal", "peaks", "instant_rr", "time"] + if self.n_channels: + for i in range(self.n_channels): + colnames.extend(["Channel_" + str(i)]) + pd.DataFrame(np.array(saveList).T, columns=colnames).to_csv( + fname, index=False + ) + else: + recording = np.array(saveList) + np.save(fname, recording) - def setup(self, read_duration=1, clear_peaks=True): + def setup( + self, read_duration: float = 1.0, clear_peaks: bool = True, nAttempts: int = 100 + ): """Find start byte and read a portion of signal. Parameters @@ -358,6 +472,9 @@ def setup(self, read_duration=1, clear_peaks=True): Length of signal to record after setup. Default is set to 1 second. clear_peaks : bool If *True*, will remove detected peaks. + nAttempts : int + Number of attempts to read pulse oximeter signal from the USB. If no + readable signal has been receive after `nAttemps`, a RuntimeError is raised. Notes ----- @@ -366,12 +483,23 @@ def setup(self, read_duration=1, clear_peaks=True): procedure are automatically removed. """ # Reset recording instance - self.__init__(serial=self.serial, add_channels=self.n_channels) + self.reset( + serial=self.serial, + add_channels=self.n_channels, + data_format=self.data_format, + ) + completed, i = False, 0 while True: + i += 1 self.serial.reset_input_buffer() paquet = list(self.serial.read(5)) if self.check(paquet=paquet): + completed = True break + if i > nAttempts: + break + if completed is False: + raise RuntimeError("Unable to read signal from the USB port.") self.read(duration=read_duration) # Remove peaks @@ -381,22 +509,21 @@ def setup(self, read_duration=1, clear_peaks=True): return self def waitBeat(self): - """Read Oximeter until a heartbeat is detected. - """ + """Read Oximeter until a heartbeat is detected.""" while True: if self.serial.inWaiting() >= 5: # Store Oxi level paquet = list(self.serial.read(5)) if self.check(paquet): - self.add_paquet(paquet[2]) + self.add_paquet(value=self.get_value(paquet)) if any(self.peaks[-2:]): # Peak found break else: - print('Synch error') + print("Synch error") return self -class BrainVisionExG(): +class BrainVisionExG: """Recording ECG signal through TCPIP. Parameters @@ -429,8 +556,7 @@ class BrainVisionExG(): Notes ----- This class is adapted from the RDA client for python made available by - Brain Products on the following link: - https://www.brainproducts.com/downloads.php?kid=2 + Brain Products on the following link: https://www.brainproducts.com/downloads.php?kid=2 """ def __init__(self, ip, sfreq, port=51244): @@ -451,12 +577,17 @@ def __init__(self, ip, sfreq, port=51244): self.con.connect((self.ip, self.port)) # Marker dict for storing marker information - self.marker = {'position': 0, 'points': 0, 'channel': -1, - 'type': "", 'description': ""} + self.marker = { + "position": 0, + "points": 0, + "channel": -1, + "type": "", + "description": "", + } def RecvData(self, requestedSize): """Helper function for receiving whole message""" - returnStream = b'' + returnStream = b"" while len(returnStream) < requestedSize: databytes = self.con.recv(requestedSize - len(returnStream)) if not databytes: @@ -473,7 +604,7 @@ def SplitString(self, raw): stringlist = [] s = "" for i in range(len(raw)): - if raw[i] != '\x00': + if raw[i] != "\x00": s = s + raw[i] else: stringlist.append(s) @@ -485,17 +616,17 @@ def GetProperties(self, rawdata): read from tcpip socket""" # Extract numerical data - (channelCount, samplingInterval) = unpack(' self.lastBlock + 1: - print("*** Overflow with " + - str(block - self.lastBlock) + " datablocks ***") + print( + "*** Overflow with " + + str(block - self.lastBlock) + + " datablocks ***" + ) self.lastBlock = block # Print markers, if there are some in actual block if markerCount > 0: for m in range(markerCount): - print("Marker " + markers[m]['description'] + - " of type " + markers[m]['type']) + print( + "Marker " + + markers[m]["description"] + + " of type " + + markers[m]["type"] + ) # Put data at the end of actual buffer self.recording.extend(data) - if ((len(self.recording)/self.sfreq)/channelCount) >= duration: + if ((len(self.recording) / self.sfreq) / channelCount) >= duration: break elif msgtype == 3: # Stop message, terminate program @@ -626,12 +775,12 @@ def findOximeter(): usbList = list(list_ports.comports()) for usb in usbList: - print('Connecting on device found in USB port ' + usb.device) + print("Connecting on device found in USB port " + usb.device) try: - Oximeter(serial=serial.Serial(usb.device)).setup().read(.2) + Oximeter(serial=serial.Serial(usb.device)).setup().read(0.2) port = usb.device except: - print('Invalid signal.') + print("Invalid signal.") return port diff --git a/systole/tests/test_correction.py b/systole/tests/test_correction.py index f907867d..73205b66 100644 --- a/systole/tests/test_correction.py +++ b/systole/tests/test_correction.py @@ -1,17 +1,24 @@ # Author: Nicolas Legrand -import numpy as np import unittest from unittest import TestCase -from systole.correction import correct_extra, correct_missed, \ - correct_rr, interpolate_bads, correct_peaks, correct_extra_peaks, \ - correct_missed_peaks + +import numpy as np + from systole import import_rr +from systole.correction import ( + correct_extra, + correct_extra_peaks, + correct_missed, + correct_missed_peaks, + correct_peaks, + correct_rr, + interpolate_bads, +) from systole.utils import simulate_rr class TestDetection(TestCase): - def test_correct_extra(self): """Test oxi_peaks function""" rr = import_rr().rr.values # Import RR time series @@ -19,7 +26,7 @@ def test_correct_extra(self): clean_rr = correct_extra(rr, 20) clean_rr = correct_extra(rr, len(rr)) clean_rr = correct_extra(list(rr), 20) - assert len(clean_rr) == len(rr)-1 + assert len(clean_rr) == len(rr) - 1 assert rr.std() > clean_rr.std() def test_correct_missed(self): @@ -28,7 +35,7 @@ def test_correct_missed(self): rr[20] = 1600 clean_rr = correct_missed(rr, 20) clean_rr = correct_missed(list(rr), 20) - assert len(clean_rr) == len(rr)+1 + assert len(clean_rr) == len(rr) + 1 def test_interpolate_bads(self): """Test interpolate_bads function""" @@ -43,24 +50,24 @@ def test_correct_rr(self): rr = simulate_rr() # Import RR time series correction = correct_rr(rr) correction = correct_rr(list(rr)) - assert len(correction['clean_rr']) == 352 - assert correction['ectopic'] == 5 - assert correction['missed'] == 1 - assert correction['extra'] == 1 - assert correction['long'] == 1 - assert correction['short'] == 3 + assert len(correction["clean_rr"]) == 352 + assert correction["ectopic"] == 5 + assert correction["missed"] == 1 + assert correction["extra"] == 1 + assert correction["long"] == 1 + assert correction["short"] == 3 def test_correct_peaks(self): """Test correct_peaks function""" peaks = simulate_rr(as_peaks=True) peaks_correction = correct_peaks(peaks) peaks_correction = correct_peaks(list(peaks)) - assert len(peaks_correction['clean_peaks']) == 280154 - assert peaks_correction['missed'] == 1 - assert peaks_correction['extra'] == 1 - assert peaks_correction['ectopic'] == 0 - assert peaks_correction['long'] == 0 - assert peaks_correction['short'] == 0 + assert len(peaks_correction["clean_peaks"]) == 280154 + assert peaks_correction["missed"] == 1 + assert peaks_correction["extra"] == 1 + assert peaks_correction["ectopic"] == 0 + assert peaks_correction["long"] == 0 + assert peaks_correction["short"] == 0 def test_correct_extra_peaks(self): """Test correct_extra_peaks function""" @@ -69,7 +76,7 @@ def test_correct_extra_peaks(self): peaks_correction = correct_extra_peaks(peaks, 20) peaks_correction = correct_extra_peaks(list(peaks), 20) assert len(peaks_correction) == len(peaks) - assert sum(peaks_correction) == sum(peaks)-1 + assert sum(peaks_correction) == sum(peaks) - 1 def test_correct_missed_peaks(self): """Test correct_missed_peaks function""" @@ -78,8 +85,8 @@ def test_correct_missed_peaks(self): peaks_correction = correct_missed_peaks(peaks, 20) peaks_correction = correct_missed_peaks(list(peaks), 20) assert len(peaks_correction) == len(peaks) - assert sum(peaks_correction) == sum(peaks)+1 + assert sum(peaks_correction) == sum(peaks) + 1 -if __name__ == '__main__': - unittest.main(argv=['first-arg-is-ignored'], exit=False) +if __name__ == "__main__": + unittest.main(argv=["first-arg-is-ignored"], exit=False) diff --git a/systole/tests/test_detection.py b/systole/tests/test_detection.py index b3280fbf..862c1492 100644 --- a/systole/tests/test_detection.py +++ b/systole/tests/test_detection.py @@ -1,29 +1,29 @@ # Author: Nicolas Legrand -import numpy as np import unittest from unittest import TestCase -from systole.detection import oxi_peaks, rr_artefacts, interpolate_clipping,\ - ecg_peaks -from systole import import_ppg, import_dataset + +import numpy as np + +from systole import import_dataset1, import_ppg +from systole.detection import ecg_peaks, interpolate_clipping, oxi_peaks, rr_artefacts from systole.utils import simulate_rr class TestDetection(TestCase): - def test_oxi_peaks(self): """Test oxi_peaks function""" df = import_ppg() # Import PPG recording - signal, peaks = oxi_peaks(df.ppg.to_numpy()) + signal, peaks = oxi_peaks(df.ppg.to_numpy(), clean_extra=True) assert len(signal) == len(peaks) assert np.all(np.unique(peaks) == [0, 1]) + assert np.mean(np.where(peaks)[0]) == 165778.0 def test_rr_artefacts(self): rr = simulate_rr() # Simulate RR time series artefacts = rr_artefacts(rr) artefacts = rr_artefacts(list(rr)) - assert all( - 350 == x for x in [len(artefacts[k]) for k in artefacts.keys()]) + assert all(350 == x for x in [len(artefacts[k]) for k in artefacts.keys()]) def test_interpolate_clipping(self): df = import_ppg() @@ -36,19 +36,27 @@ def test_interpolate_clipping(self): clean_signal = interpolate_clipping(df.ppg.to_numpy()) def test_ecg_peaks(self): - signal_df = import_dataset()[:20*2000] - signal, peaks = ecg_peaks(signal_df.ecg.to_numpy(), method='hamilton', - sfreq=2000, find_local=True) - for method in ['christov', 'engelse-zeelenberg', 'pan-tompkins', - 'wavelet-transform', 'moving-average']: - signal, peaks = ecg_peaks(signal_df.ecg, method=method, sfreq=2000, - find_local=True) + signal_df = import_dataset1()[: 20 * 2000] + signal, peaks = ecg_peaks( + signal_df.ecg.to_numpy(), method="hamilton", sfreq=2000, find_local=True + ) + for method in [ + "christov", + "engelse-zeelenberg", + "pan-tompkins", + "wavelet-transform", + "moving-average", + ]: + signal, peaks = ecg_peaks( + signal_df.ecg, method=method, sfreq=2000, find_local=True + ) assert not np.any(peaks > 1) with self.assertRaises(ValueError): - signal, peaks = ecg_peaks(signal_df.ecg.to_numpy(), method='error', - sfreq=2000, find_local=True) + signal, peaks = ecg_peaks( + signal_df.ecg.to_numpy(), method="error", sfreq=2000, find_local=True + ) -if __name__ == '__main__': - unittest.main(argv=['first-arg-is-ignored'], exit=False) +if __name__ == "__main__": + unittest.main(argv=["first-arg-is-ignored"], exit=False) diff --git a/systole/tests/test_hrv.py b/systole/tests/test_hrv.py index bbdcc13e..ad150b3d 100644 --- a/systole/tests/test_hrv.py +++ b/systole/tests/test_hrv.py @@ -1,19 +1,19 @@ # Author: Nicolas Legrand -import pandas as pd -import numpy as np import unittest -import pytest from unittest import TestCase -from systole.hrv import nnX, pnnX, rmssd, time_domain,\ - frequency_domain, nonlinear + +import numpy as np +import pandas as pd +import pytest + from systole import import_rr +from systole.hrv import frequency_domain, nnX, nonlinear, pnnX, rmssd, time_domain rr = import_rr().rr.values class TestHrv(TestCase): - def test_nnX(self): """Test nnX function""" nn = nnX(list(rr)) @@ -56,5 +56,5 @@ def test_nonlinear(self): self.assertEqual(stats.size, 4) -if __name__ == '__main__': - unittest.main(argv=['first-arg-is-ignored'], exit=False) +if __name__ == "__main__": + unittest.main(argv=["first-arg-is-ignored"], exit=False) diff --git a/systole/tests/test_plotly.py b/systole/tests/test_plotly.py index 17491d78..b91e7aff 100644 --- a/systole/tests/test_plotly.py +++ b/systole/tests/test_plotly.py @@ -1,29 +1,36 @@ # Author: Nicolas Legrand -import pandas as pd -import numpy as np import unittest from unittest import TestCase -from systole.plotly import plot_raw, plot_shortLong, plot_ectopic, \ - plot_subspaces, plot_frequency, plot_nonlinear, plot_timedomain -from systole import import_ppg, import_dataset + +import numpy as np +import pandas as pd + +from systole import import_dataset1, import_ppg +from systole.plotly import ( + plot_ectopic, + plot_frequency, + plot_nonlinear, + plot_raw, + plot_shortLong, + plot_subspaces, + plot_timedomain, +) from systole.utils import simulate_rr rr = simulate_rr() ppg = import_ppg().ppg.to_numpy() -signal_df = pd.DataFrame({'time': np.arange(0, len(ppg))/75, - 'ppg': ppg}) +signal_df = pd.DataFrame({"time": np.arange(0, len(ppg)) / 75, "ppg": ppg}) class TestInteractive(TestCase): - def test_plot_raw(self): """Test plot_raw function""" plot_raw(signal_df) plot_raw(signal_df.ppg.to_numpy()) - ecg_df = import_dataset()[:20*2000] - plot_raw(ecg_df, type='ecg') - plot_raw(ecg_df.ecg.to_numpy(), type='ecg', sfreq=2000) + ecg_df = import_dataset1()[: 20 * 2000] + plot_raw(ecg_df, type="ecg") + plot_raw(ecg_df.ecg.to_numpy(), type="ecg", sfreq=2000) def test_plot_shortLong(self): """Test plot_shortLong function""" @@ -53,5 +60,6 @@ def test_plot_timedomain(self): """Test nnX function""" plot_timedomain(rr) -if __name__ == '__main__': - unittest.main(argv=['first-arg-is-ignored'], exit=False) + +if __name__ == "__main__": + unittest.main(argv=["first-arg-is-ignored"], exit=False) diff --git a/systole/tests/test_plotting.py b/systole/tests/test_plotting.py index f9bd399f..6b601ccb 100644 --- a/systole/tests/test_plotting.py +++ b/systole/tests/test_plotting.py @@ -1,20 +1,28 @@ # Author: Nicolas Legrand +import unittest +from unittest import TestCase + +import matplotlib import numpy as np import pandas as pd -import unittest 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 +from systole.plotting import ( + circular, + plot_circular, + plot_events, + plot_oximeter, + plot_psd, + plot_raw, + plot_subspaces, +) from systole.recording import Oximeter - serial = serialSim() oxi = Oximeter(serial=serial, add_channels=1).setup().read(10) -oxi.channels['Channel_0'][100] = 1 +oxi.channels["Channel_0"][100] = 1 # Simulate oximeter instance from recorded signal ppg = import_ppg().ppg.to_numpy() @@ -24,29 +32,23 @@ oxi.instant_rr = [0] * 75 oxi.recording = list(ppg[:75]) for i in range(len(ppg[75:750])): - oxi.add_paquet(ppg[75+i]) -oxi.channels['Channel_0'] = np.zeros(750, dtype=int) -oxi.channels['Channel_0'][np.random.choice(np.arange(0, 750), 5)] = 1 -oxi.times = list(np.arange(0, 10, 1/75)) + oxi.add_paquet(ppg[75 + i]) +oxi.channels["Channel_0"] = np.zeros(750, dtype=int) +oxi.channels["Channel_0"][np.random.choice(np.arange(0, 750), 5)] = 1 +oxi.times = list(np.arange(0, 10, 1 / 75)) # Create angular data x = np.random.normal(np.pi, 0.5, 100) -y = np.random.uniform(0, np.pi*2, 100) -z = np.concatenate([np.random.normal(np.pi/2, 0.5, 50), - np.random.normal(np.pi + np.pi/2, 0.5, 50)]) +y = np.random.uniform(0, np.pi * 2, 100) +z = np.concatenate( + [np.random.normal(np.pi / 2, 0.5, 50), np.random.normal(np.pi + np.pi / 2, 0.5, 50)] +) class TestPlotting(TestCase): - - def test_plot_hr(self): - ax = plot_hr(oxi) - assert isinstance(ax, matplotlib.axes.Axes) - ax = plot_hr(oxi.peaks) - assert isinstance(ax, matplotlib.axes.Axes) - outliers = np.where(oxi.peaks)[0][:2] - ax = plot_hr(np.asarray(oxi.peaks), unit='bpm', outliers=outliers) - assert isinstance(ax, matplotlib.axes.Axes) - ax = plot_hr([600, 650, 700, 750]) + def test_plot_raw(self): + ax = plot_raw(ppg) + assert isinstance(ax[0], matplotlib.axes.Axes) def test_plot_events(self): ax = plot_events(oxi) @@ -76,23 +78,22 @@ def test_circular(self): """Tests _circular function""" ax = circular(list(x)) assert isinstance(ax, matplotlib.axes.Axes) - for dens in ['area', 'heigth', 'alpha']: - ax = circular(x, density='alpha', offset=np.pi, ax=None) + for dens in ["area", "heigth", "alpha"]: + ax = circular(x, density="alpha", offset=np.pi, ax=None) assert isinstance(ax, matplotlib.axes.Axes) - ax = circular(x, density='height', mean=True, - units='degree', color='r') + ax = circular(x, density="height", mean=True, units="degree", color="r") assert isinstance(ax, matplotlib.axes.Axes) with pytest.raises(ValueError): - ax = circular(x, density='xx') + ax = circular(x, density="xx") def test_plot_circular(self): """Test plot_circular function""" - data = pd.DataFrame(data={'x': x, 'y': y, 'z': z}).melt() - ax = plot_circular(data=data, y='value', hue='variable') + data = pd.DataFrame(data={"x": x, "y": y, "z": z}).melt() + ax = plot_circular(data=data, y="value", hue="variable") assert isinstance(ax, matplotlib.axes.Axes) - ax = plot_circular(data=data, y='value', hue=None) + ax = plot_circular(data=data, y="value", hue=None) assert isinstance(ax, matplotlib.axes.Axes) -if __name__ == '__main__': - unittest.main(argv=['first-arg-is-ignored'], exit=False) +if __name__ == "__main__": + unittest.main(argv=["first-arg-is-ignored"], exit=False) diff --git a/systole/tests/test_recording.py b/systole/tests/test_recording.py index 1d367deb..4760baf3 100644 --- a/systole/tests/test_recording.py +++ b/systole/tests/test_recording.py @@ -1,18 +1,19 @@ # Author: Nicolas Legrand import os -import unittest +import socket import threading +import unittest +from unittest import TestCase + import matplotlib -import socket import numpy as np -from unittest import TestCase + from systole import serialSim -from systole.recording import Oximeter, BrainVisionExG +from systole.recording import BrainVisionExG, Oximeter class TestRecording(TestCase): - def test_oximeter(self): serial = serialSim() @@ -21,14 +22,24 @@ def test_oximeter(self): serial.ppg = serial.ppg[-2:] # To the end of recording oxi.read(10) oxi.find_peaks() + + oxi.save("test") + assert os.path.exists("test.npy") + os.remove("test.npy") + + oxi.save("test.txt") + assert os.path.exists("test.txt") + os.remove("test.txt") + # Simulate events in recording for idx in np.random.choice(len(oxi.recording), 5): - oxi.channels['Channel_0'][idx] = 1 + oxi.channels["Channel_0"][idx] = 1 ax = oxi.plot_events() assert isinstance(ax, matplotlib.axes.Axes) - ax = oxi.plot_hr() - assert isinstance(ax, matplotlib.axes.Axes) + ax = oxi.plot_raw() + assert isinstance(ax[0], matplotlib.axes.Axes) + assert isinstance(ax[1], matplotlib.axes.Axes) ax = oxi.plot_recording() assert isinstance(ax, matplotlib.axes.Axes) @@ -50,22 +61,17 @@ def test_oximeter(self): oxi.times = [] oxi.threshold = [] - oxi.save('test') - assert os.path.exists("test.npy") - os.remove("test.npy") - oxi = Oximeter(serial=serial) - def test_BrainVisionExG(self): - data1 = b'\x8eEXC\x96\xc9\x86L\xafJ\x98\xbb\xf6\xc9\x14P\x8d\x00\x00\x00\x01\x00\x00\x00' - data2 = b'\x08\x00\x00\x00\x00\x00\x00\x00\x00@\x8f@\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?33333\x13c@33333\x13c@EGG1\x00EGG2\x00EGG3\x00EGG4\x00EGG5\x00EGG6\x00RESP\x00PLETH\x00' - data3 = b'\x8eEXC\x96\xc9\x86L\xafJ\x98\xbb\xf6\xc9\x14P\xa4\x02\x00\x00\x04\x00\x00\x00' - data4 = b'\xad)\x01\x00\x14\x00\x00\x00\x00\x00\x00\x00\x00\x04.F\x00\xc8\x9bE\x00\xb0HE\x00|\x05F\x00\x00IC\x00\x00!\xc3\x00`}F\x00@mE\x00\x90$F\x00\xe8\x93E\x00\xc0pE\x00\xb0\xbbE\x00\x00\xe8\xc2\x00\x00,\xc3\x00`}F\x00 mE\x00\xf8\xfbE\x00`\xdbD\x00@\tD\x00@\xa4E\x00\x00\x00\xc2\x00\x00\x1b\xc3\x00\\}F\x00\x00mE\x00\\]F\x000lE\x00\x18\x81E\x00\x08\x0bF\x00\x00H\xc3\x00\x00,\xc3\x00`}F\x00\x00mE\x00\x98\xeaE\x00\x88\xd1\xc5\x00`\xcdD\x00|Q\xc6\x00\x003\xc4\x00\x00\x00\xc3\x00`}F\x00\x00mE\x00\xf4\x18F\x00\x90L\xc5\x00\xf0\x15E\x00@\xe6\xc5\x00\x80T\xc4\x00\x00(\xc3\x00h}F\x00 mE\x00P\xfeE\x00\xd0\xcd\xc5\x00\x80N\xc5\x00\x80\xc0\xc4\x00\x80W\xc4\x00\x00*\xc3\x00h}F\x00@mE\x00\x90\xdcE\x00@\x07\xc6\x000\xea\xc5\x00\xf8\xa3E\x00\xc0l\xc4\x00\x00?\xc3\x00`}F\x00@mE\x00\xd8\xf0E\x00\x00\xe9\xc5\x00\xe8\xb2\xc5\x00\xd0eE\x00\x80f\xc4\x00\x00f\xc3\x00`}F\x00\x00mE\x00\xbc\nF\x00p\x99\xc5\x00\x00\xed\xc3\x00\xd0}\xc5\x00@S\xc4\x00\x00g\xc3\x00`}F\x00\x00mE\x00\xdc\x02F\x00\x80\xb2\xc5\x00`\x02\xc5\x00@\xfd\xc4\x00\x00Y\xc4\x00\x00s\xc3\x00h}F\x00\x00mE\x00\xe8\x04F\x00X\xac\xc5\x00 \xcf\xc4\x00`C\xc5\x00\x00U\xc4\x00\x00t\xc3\x00`}F\x00 mE\x00x\x01F\x00H\xbe\xc5\x00\x10%\xc5\x00`\xf1\xc4\x00\x80V\xc4\x00\x00q\xc3\x00`}F\x00 mE\x00\xb8\xd0E\x00P\x15\xc6\x00\xf0\x12\xc6\x00`\xfdE\x00\x80m\xc4\x00\x00^\xc3\x00`}F\x00@mE\x00|\x05F\x00H\xbf\xc5\x00\xf0%\xc5\x00@\x84\xc4\x00@S\xc4\x00\x00,\xc3\x00`}F\x00\x10mE\x008\x0fF\x00(\x8f\xc5\x00\x00\x85\xc3\x00\xd0\x8a\xc5\x00\x80K\xc4\x00\x00#\xc3\x00\\}F\x00\x00mE\x00\x84\x85F\x00\x90HF\x00\x80PE\x00@RD\x00@T\xc4\x00\x00\x13\xc3\x00`}F\x00@lE\x00x\x8eE\x00(\x8dE\x00\x80\xb0\xc5\x00\xe0\xbbE\x00\xc0Q\xc4\x00\x00@\xc3\x00`}F\x00\xd0jE\x00@\xc8D\x00\xe8b\xc6\x000\xdb\xc5\x00\x90\xc7\xc5\x00\xc0Q\xc4\x00\x00l\xc3\x00`}F\x00piE\x00t*F\x00\xd0)\xc5\x00\x00\x02C\x00\xc0\x1d\xc5\x00\xc0W\xc4\x00\x00a\xc3\x00`}F\x00\x00hE' + data1 = b"\x8eEXC\x96\xc9\x86L\xafJ\x98\xbb\xf6\xc9\x14P\x8d\x00\x00\x00\x01\x00\x00\x00" + data2 = b"\x08\x00\x00\x00\x00\x00\x00\x00\x00@\x8f@\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xe0?33333\x13c@33333\x13c@EGG1\x00EGG2\x00EGG3\x00EGG4\x00EGG5\x00EGG6\x00RESP\x00PLETH\x00" + data3 = b"\x8eEXC\x96\xc9\x86L\xafJ\x98\xbb\xf6\xc9\x14P\xa4\x02\x00\x00\x04\x00\x00\x00" + data4 = b"\xad)\x01\x00\x14\x00\x00\x00\x00\x00\x00\x00\x00\x04.F\x00\xc8\x9bE\x00\xb0HE\x00|\x05F\x00\x00IC\x00\x00!\xc3\x00`}F\x00@mE\x00\x90$F\x00\xe8\x93E\x00\xc0pE\x00\xb0\xbbE\x00\x00\xe8\xc2\x00\x00,\xc3\x00`}F\x00 mE\x00\xf8\xfbE\x00`\xdbD\x00@\tD\x00@\xa4E\x00\x00\x00\xc2\x00\x00\x1b\xc3\x00\\}F\x00\x00mE\x00\\]F\x000lE\x00\x18\x81E\x00\x08\x0bF\x00\x00H\xc3\x00\x00,\xc3\x00`}F\x00\x00mE\x00\x98\xeaE\x00\x88\xd1\xc5\x00`\xcdD\x00|Q\xc6\x00\x003\xc4\x00\x00\x00\xc3\x00`}F\x00\x00mE\x00\xf4\x18F\x00\x90L\xc5\x00\xf0\x15E\x00@\xe6\xc5\x00\x80T\xc4\x00\x00(\xc3\x00h}F\x00 mE\x00P\xfeE\x00\xd0\xcd\xc5\x00\x80N\xc5\x00\x80\xc0\xc4\x00\x80W\xc4\x00\x00*\xc3\x00h}F\x00@mE\x00\x90\xdcE\x00@\x07\xc6\x000\xea\xc5\x00\xf8\xa3E\x00\xc0l\xc4\x00\x00?\xc3\x00`}F\x00@mE\x00\xd8\xf0E\x00\x00\xe9\xc5\x00\xe8\xb2\xc5\x00\xd0eE\x00\x80f\xc4\x00\x00f\xc3\x00`}F\x00\x00mE\x00\xbc\nF\x00p\x99\xc5\x00\x00\xed\xc3\x00\xd0}\xc5\x00@S\xc4\x00\x00g\xc3\x00`}F\x00\x00mE\x00\xdc\x02F\x00\x80\xb2\xc5\x00`\x02\xc5\x00@\xfd\xc4\x00\x00Y\xc4\x00\x00s\xc3\x00h}F\x00\x00mE\x00\xe8\x04F\x00X\xac\xc5\x00 \xcf\xc4\x00`C\xc5\x00\x00U\xc4\x00\x00t\xc3\x00`}F\x00 mE\x00x\x01F\x00H\xbe\xc5\x00\x10%\xc5\x00`\xf1\xc4\x00\x80V\xc4\x00\x00q\xc3\x00`}F\x00 mE\x00\xb8\xd0E\x00P\x15\xc6\x00\xf0\x12\xc6\x00`\xfdE\x00\x80m\xc4\x00\x00^\xc3\x00`}F\x00@mE\x00|\x05F\x00H\xbf\xc5\x00\xf0%\xc5\x00@\x84\xc4\x00@S\xc4\x00\x00,\xc3\x00`}F\x00\x10mE\x008\x0fF\x00(\x8f\xc5\x00\x00\x85\xc3\x00\xd0\x8a\xc5\x00\x80K\xc4\x00\x00#\xc3\x00\\}F\x00\x00mE\x00\x84\x85F\x00\x90HF\x00\x80PE\x00@RD\x00@T\xc4\x00\x00\x13\xc3\x00`}F\x00@lE\x00x\x8eE\x00(\x8dE\x00\x80\xb0\xc5\x00\xe0\xbbE\x00\xc0Q\xc4\x00\x00@\xc3\x00`}F\x00\xd0jE\x00@\xc8D\x00\xe8b\xc6\x000\xdb\xc5\x00\x90\xc7\xc5\x00\xc0Q\xc4\x00\x00l\xc3\x00`}F\x00piE\x00t*F\x00\xd0)\xc5\x00\x00\x02C\x00\xc0\x1d\xc5\x00\xc0W\xc4\x00\x00a\xc3\x00`}F\x00\x00hE" def simulateEXG(): # Run a server simulating BrainVisionExG amplifier connection server = socket.socket() - server.bind(('127.0.0.1', 51244)) + server.bind(("127.0.0.1", 51244)) server.listen(0) conn, addr = server.accept() with conn: @@ -79,13 +85,21 @@ def simulateEXG(): server_thread.start() # Test the clients basic connection and disconnection - recorder = BrainVisionExG('127.0.0.1', port=51244, sfreq=1000) - data = recorder.read(.0001) + recorder = BrainVisionExG("127.0.0.1", port=51244, sfreq=1000) + data = recorder.read(0.0001) assert list(data.keys()) == [ - 'EGG1', 'EGG2', 'EGG3', 'EGG4', 'EGG5', 'EGG6', 'RESP', 'PLETH'] + "EGG1", + "EGG2", + "EGG3", + "EGG4", + "EGG5", + "EGG6", + "RESP", + "PLETH", + ] assert all([data[k].shape[0] == 20 for k in list(data.keys())]) -if __name__ == '__main__': - unittest.main(argv=['first-arg-is-ignored'], exit=False) +if __name__ == "__main__": + unittest.main(argv=["first-arg-is-ignored"], exit=False) diff --git a/systole/tests/test_utils.py b/systole/tests/test_utils.py index 3cf4b9d8..e86ed1fb 100644 --- a/systole/tests/test_utils.py +++ b/systole/tests/test_utils.py @@ -1,32 +1,40 @@ # Author: Nicolas Legrand -import pytest import unittest -import numpy as np -from systole.utils import norm_triggers, heart_rate, to_angles, to_epochs,\ - time_shift, simulate_rr, to_rr -from systole.detection import oxi_peaks from unittest import TestCase + +import numpy as np +import pytest + from systole import import_ppg, import_rr +from systole.detection import oxi_peaks +from systole.utils import ( + heart_rate, + norm_triggers, + simulate_rr, + time_shift, + to_angles, + to_epochs, + to_rr, +) class TestUtils(TestCase): - def test_norm_triggers(self): ppg = import_ppg().ppg.to_numpy() # Import PPG recording signal, peaks = oxi_peaks(ppg) - peaks[np.where(peaks)[0]+1] = 1 - peaks[np.where(peaks)[0]+2] = 1 + 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') + 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') + norm_triggers(peaks, direction="invalid") def test_heart_rate(self): """Test heart_rate function""" @@ -36,8 +44,7 @@ def test_heart_rate(self): 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) + 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]) @@ -45,7 +52,7 @@ def test_heart_rate(self): def test_time_shift(self): """Test time_shift function""" lag = time_shift([40, 50, 60], [45, 52]) - assert lag == [5, 2] + assert np.all(lag == [5, 2]) def test_to_angle(self): """Test to_angles function""" @@ -64,15 +71,14 @@ def test_to_epochs(self): ppg = import_ppg().ppg.to_numpy() # Import PPG recording events = import_ppg().ppg.to_numpy() # Import events events[2] = 1 - epochs = to_epochs(ppg, events, sfreq=75, verbose=True, - apply_baseline=(-1, 0)) + epochs = to_epochs(ppg, events, sfreq=75, verbose=True, apply_baseline=(-1, 0)) assert epochs.ndim == 2 - epochs = to_epochs(list(ppg), list(events), sfreq=75, - apply_baseline=None) + epochs = to_epochs(list(ppg), list(events), sfreq=75, apply_baseline=None) reject = np.arange(0, len(ppg)) reject[50:55] = 1 - epochs = to_epochs(ppg, events, sfreq=75, apply_baseline=-1, - reject=reject, verbose=True) + epochs = to_epochs( + ppg, events, sfreq=75, apply_baseline=-1, reject=reject, verbose=True + ) with pytest.raises(ValueError): epochs = to_epochs(ppg[1:], events, sfreq=75) @@ -91,5 +97,5 @@ def test_to_rr(self): assert rr.mean() == 874.2068965517242 -if __name__ == '__main__': - unittest.main(argv=['first-arg-is-ignored'], exit=False) +if __name__ == "__main__": + unittest.main(argv=["first-arg-is-ignored"], exit=False) diff --git a/systole/utils.py b/systole/utils.py index 8e51b936..a0324fba 100644 --- a/systole/utils.py +++ b/systole/utils.py @@ -1,17 +1,24 @@ # Author: Nicolas Legrand +from typing import List, Optional, Tuple, Union + import numpy as np from scipy.interpolate import interp1d -def norm_triggers(x, threshold=1, n=5, direction='higher'): - """Turns noisy triggers into unique boolean. +def norm_triggers( + x: Union[List, np.ndarray], + threshold: int = 1, + n: int = 5, + direction: str = "higher", +) -> Union[List, np.ndarray]: + """Turns noisy triggers into boolean. Keep the first trigger and set to 0 the n following values. Parameters ---------- - x : 1d array-like + x : np.ndarray or list The triggers to convert. threshold : float Threshold for triggering values. Default is 1. @@ -23,45 +30,47 @@ def norm_triggers(x, threshold=1, n=5, direction='higher'): Returns ------- - y : 1d array-like - The filterd triggers + y : np.ndarray + The filterd triggers array. """ if not isinstance(x, np.ndarray): - raise ValueError('x must be a Numpy array') + raise ValueError("x must be a Numpy array") - if direction == 'higher': + if direction == "higher": y = x >= threshold - elif direction == 'lower': + elif direction == "lower": y = x <= threshold else: - raise ValueError('Invalid direction') + raise ValueError("Invalid direction") # Keep only the first trigger in window size for i in range(len(y)): if y[i]: if (len(y) - i) < n: # If close to the end - y[i+1:] = False + y[i + 1 :] = False else: - y[i+1:i+n+1] = False + y[i + 1 : i + n + 1] = False return y -def time_shift(x, events, order='after'): +def time_shift( + x: Union[List, np.ndarray], events: Union[List, np.ndarray], order: str = "after" +) -> np.ndarray: """Return the delay between x and events. Parameters ---------- - x : 1d array-like + x : np.ndarray or list Timing of reference events. - events : 1d array-like + events : np.ndarray or list Timing of events of heartrateest. order : str Consider event occurung before of after baseline. Default is 'after'. Returns ------- - time_shift : 1d array-like - The delay between X and events (a.u) + time_shift : np.ndarray + The delay between X and events (a.u). """ if isinstance(x, list): x = np.asarray(x) @@ -75,15 +84,17 @@ def time_shift(x, events, order='after'): # Event timing lag.append(e - r) - return lag + return np.array(lag) -def heart_rate(x, sfreq=1000, unit='rr', kind='cubic'): +def heart_rate( + x: Union[List, np.ndarray], sfreq: int = 1000, unit: str = "rr", kind: str = "cubic" +) -> Tuple[np.ndarray, np.ndarray]: """Transform peaks data into heart rate time series. Parameters ---------- - x : 1d array-like + x : np.ndarray or list Boolean vector of peaks detection. sfreq : int Sampling frequency. @@ -95,9 +106,9 @@ def heart_rate(x, sfreq=1000, unit='rr', kind='cubic'): Returns ------- - heartrate : 1d array-like + heartrate : np.ndarray The heart rate frequency. - time : 1d array-like + time : np.ndarray Time array. Notes @@ -110,42 +121,45 @@ def heart_rate(x, sfreq=1000, unit='rr', kind='cubic'): if isinstance(x, list): x = np.asarray(x) if not ((x == 1) | (x == 0)).all(): - raise ValueError('Input vector should only contain 0 and 1') + raise ValueError("Input vector should only contain 0 and 1") # Find peak indices peaks_idx = np.where(x)[0] # Create time vector (seconds): - time = (peaks_idx/sfreq)[1:] + time = (peaks_idx / sfreq)[1:] rr = np.diff(peaks_idx) # R-R heartratevals (in miliseconds) heartrate = (rr / sfreq) * 1000 - if unit == 'bpm': + if unit == "bpm": # Beats per minutes - heartrate = (60000 / heartrate) + heartrate = 60000 / heartrate # Use the peaks vector as time input - new_time = np.arange(0, len(x)/sfreq, 1/sfreq) + new_time = np.arange(0, len(x) / sfreq, 1 / sfreq) if kind is not None: # Interpolate - f = interp1d(time, heartrate, kind=kind, bounds_error=False, - fill_value=(np.nan, np.nan)) + f = interp1d( + time, heartrate, kind=kind, bounds_error=False, fill_value=(np.nan, np.nan) + ) heartrate = f(new_time) return heartrate, new_time -def to_angles(x, events): +def to_angles( + x: Union[List, np.ndarray], events: Union[List, np.ndarray] +) -> np.ndarray: """Angular values of events according to x cycle peaks. Parameters ---------- - x : list or 1d array-like + x : np.ndarray or list The reference time serie. Time points can be unevenly spaced. - events : list or 1d array-like + events : np.ndarray or list The events time serie. Returns @@ -153,10 +167,8 @@ def to_angles(x, events): ang : numpy array The angular value of events in the cycle of interest (radians). """ - if isinstance(x, list): - x = np.asarray(x) - if isinstance(events, list): - events = np.asarray(events) + x = np.asarray(x) + events = np.asarray(events) # If data is provided in bollean format if not any(x > 1): @@ -166,7 +178,7 @@ def to_angles(x, events): ang = [] # Where to store angular data for i in events: - if (i >= x.min()) & (i < x.max()): + if (i >= np.min(x)) & (i < np.max(x)): # Length of current R-R interval ln = np.min(x[x > i]) - np.max(x[x <= i]) @@ -175,16 +187,25 @@ def to_angles(x, events): i -= np.max(x[x <= i]) # Convert into radian [0 to pi*2] - ang.append((i*np.pi*2)/ln) + ang.append((i * np.pi * 2) / ln) - elif i == x.max(): + elif i == np.max(x): ang.append(0.0) - return ang + return np.asarray(ang) -def to_epochs(x, events, sfreq=1000, tmin=-1, tmax=10, event_val=1, - sigma=10, apply_baseline=0, verbose=False, reject=None): +def to_epochs( + x: Union[List, np.ndarray], + events: Union[List, np.ndarray], + sfreq: int = 1000, + tmin: float = -1.0, + tmax: float = 10.0, + event_val: int = 1, + apply_baseline: Optional[Union[int, Tuple]] = 0, + verbose: bool = False, + reject: Optional[np.ndarray] = None, +) -> np.ndarray: """Epoch signal based on events indices. Parameters @@ -206,7 +227,7 @@ def to_epochs(x, events, sfreq=1000, tmin=-1, tmax=10, event_val=1, mean). If *None*, no baseline is applied. verbose : boolean If True, will return warnings if epoc are droped. - reject : 1d array-like or None + reject : np.ndarray or None Segments of the signal that should be rejected. Returns @@ -215,8 +236,10 @@ def to_epochs(x, events, sfreq=1000, tmin=-1, tmax=10, event_val=1, Event * Time array. """ if len(x) != len(events): - raise ValueError("""The length of the event and signal vector - shoul match exactly""") + raise ValueError( + """The length of the event and signal vector + shoul match exactly""" + ) # To numpy array if isinstance(x, list): x = np.asarray(x) @@ -231,55 +254,63 @@ def to_epochs(x, events, sfreq=1000, tmin=-1, tmax=10, event_val=1, reject = np.zeros(len(x)) rejected = 0 - epochs = np.zeros( - shape=(len(events), ((np.abs(tmin) + np.abs(tmax)) * sfreq))) + epochs = np.zeros(shape=(len(events), int((np.abs(tmin) + np.abs(tmax)) * sfreq))) for i, ev in enumerate(events): # Security check (epochs is not outside signal limits) - if (ev+round(tmin*sfreq) < 0) | (ev+round(tmax*sfreq) > len(x)): + if (ev + round(tmin * sfreq) < 0) | (ev + round(tmax * sfreq) > len(x)): if verbose is True: - print('Drop 1 epoch due to signal limits.') + print("Drop 1 epoch due to signal limits.") rejected += 1 epochs[i, :] = np.nan # Security check (trial contain bad peak) - elif np.any(reject[ev+round(tmin*sfreq):ev+round(tmax*sfreq)]): + elif np.any(reject[ev + round(tmin * sfreq) : ev + round(tmax * sfreq)]): if verbose is True: - print('Drop 1 epoch due to artefact.') + print("Drop 1 epoch due to artefact.") rejected += 1 epochs[i, :] = np.nan else: - trial = x[ev+round(tmin*sfreq):ev+round(tmax*sfreq)] + trial = x[ev + round(tmin * sfreq) : ev + round(tmax * sfreq)] if apply_baseline is None: epochs[i, :] = trial else: if isinstance(apply_baseline, int): - baseline = x[ev+round(apply_baseline*sfreq)] + baseline = x[ev + round(apply_baseline * sfreq)] if isinstance(apply_baseline, tuple): - low = ev+round(apply_baseline[0]*sfreq) - high = ev+round(apply_baseline[1]*sfreq) + low = ev + round(apply_baseline[0] * sfreq) + high = ev + round(apply_baseline[1] * sfreq) baseline = x[low:high].mean() epochs[i, :] = trial - baseline # Print % of rejected items if (rejected > 0) & (verbose is True): - print(str(rejected) + ' trial(s) droped due to inconsistent recording') + print(str(rejected) + " trial(s) droped due to inconsistent recording") return epochs -def simulate_rr(n_rr=350, extra_idx=[50], missed_idx=[100], short_idx=[150], - long_idx=[200], ectopic1_idx=[250], ectopic2_idx=[300], - random_state=42, as_peaks=False, artefacts=True): - """ RR time series simulation with artefacts. - - n_rr : int +def simulate_rr( + n_rr: int = 350, + extra_idx: List = [50], + missed_idx: List = [100], + short_idx: List = [150], + long_idx: List = [200], + ectopic1_idx: List = [250], + ectopic2_idx: List = [300], + random_state: int = 42, + as_peaks: bool = False, + artefacts: bool = True, +) -> np.ndarray: + """RR time series simulation with artefacts. + + n_rr : int Number of RR intervals. Default is 350. extra_idx : list Index of extra interval. Default is [50]. - missed_idx : list + missed_idx : list Index of missed interval. Default is [100]. - short_idx : list + short_idx : list Index of short interval. Default is [150]. long_idx : list Index of long interval. Default is [200]. @@ -294,14 +325,14 @@ def simulate_rr(n_rr=350, extra_idx=[50], missed_idx=[100], short_idx=[150], Returns ------- - rr : 1d array-like + rr : np.ndarray The RR time series. """ np.random.seed(random_state) rr = np.array( - [800 + 50 * np.random.normal(i, .6) for i in np.sin( - np.arange(0, n_rr, 1.0))]) + [800 + 50 * np.random.normal(i, 0.6) for i in np.sin(np.arange(0, n_rr, 1.0))] + ) if artefacts is True: @@ -309,7 +340,7 @@ def simulate_rr(n_rr=350, extra_idx=[50], missed_idx=[100], short_idx=[150], if extra_idx: n_extra = 0 for i in extra_idx: - rr[i-n_extra] -= 100 + rr[i - n_extra] -= 100 rr = np.insert(rr, i, 100) n_extra += 1 @@ -334,18 +365,18 @@ def simulate_rr(n_rr=350, extra_idx=[50], missed_idx=[100], short_idx=[150], # Add ectopic beat type 1 (NPN) if ectopic1_idx: for i in ectopic1_idx: - rr[i] *= .7 - rr[i+1] *= 1.3 + rr[i] *= 0.7 + rr[i + 1] *= 1.3 # Add ectopic beat type 2 (PNP) if ectopic2_idx: for i in ectopic2_idx: rr[i] *= 1.3 - rr[i+1] *= .7 + rr[i + 1] *= 0.7 # Transform to peaks vector if needed if as_peaks is True: - peaks = np.zeros(np.cumsum(np.rint(rr).astype(int))[-1]+50) + peaks = np.zeros(np.cumsum(np.rint(rr).astype(int))[-1] + 50) peaks[(np.cumsum(np.rint(rr).astype(int)))] = 1 peaks = peaks.astype(bool) peaks[0] = True @@ -354,14 +385,16 @@ def simulate_rr(n_rr=350, extra_idx=[50], missed_idx=[100], short_idx=[150], return rr -def to_neighbour(signal, peaks, kind='max', size=50): +def to_neighbour( + signal: np.ndarray, peaks: np.ndarray, kind: str = "max", size: int = 50 +) -> np.ndarray: """Replace peaks with max/min neighbour in a given window. Parameters ---------- - signal : 1d array-like + signal : np.ndarray Signal used to maximize/minimize peaks. - peaks: 1d array-like + peaks: np.ndarray Boolean vector of peaks position. kind : str Can be 'max' or 'min'. @@ -370,31 +403,30 @@ def to_neighbour(signal, peaks, kind='max', size=50): Returns ------- - new_peaks: 1d array-like + new_peaks: np.ndarray Boolean vector of peaks position. """ new_peaks = peaks.copy() for pk in np.where(peaks)[0]: - if kind == 'max': - x = signal[pk-size:pk+size].argmax() - elif kind == 'min': - x = signal[pk-size:pk+size].argmin() + if kind == "max": + x = signal[pk - size : pk + size].argmax() + elif kind == "min": + x = signal[pk - size : pk + size].argmin() else: - raise ValueError( - 'Invalid argument, kind should be ''max'' or ''min''') + raise ValueError("Invalid argument, kind should be " "max" " or " "min" "") new_peaks[pk] = False - new_peaks[pk+(x-size)] = True + new_peaks[pk + (x - size)] = True return new_peaks -def to_rr(peaks, sfreq=1000): +def to_rr(peaks: Union[List[float], np.ndarray], sfreq: int = 1000) -> np.ndarray: """Convert peaks index to intervals time series (RR, beat-to-beat...). Parameters ---------- - peaks : 1d array-like or list + peaks : np.ndarray or list Either a boolean array or sample index. Default is *boolean*. If the input array does not only contain 0 or 1, will automatically try sample index. @@ -403,14 +435,14 @@ def to_rr(peaks, sfreq=1000): Returns ------- - rr : 1d array-like + rr : np.ndarray Interval time series (in miliseconds). """ if isinstance(peaks, list): peaks = np.asarray(peaks) if ((peaks == 1) | (peaks == 0)).all(): - rr = (np.diff(np.where(peaks)[0])/sfreq) * 1000 + rr = (np.diff(np.where(peaks)[0]) / sfreq) * 1000 else: - rr = (np.diff(peaks)/sfreq) * 1000 + rr = (np.diff(peaks) / sfreq) * 1000 return rr