Skip to content

Commit

Permalink
- Docstrings, README and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
LegrandNico committed Sep 2, 2020
1 parent 8444785 commit 07ad050
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 138 deletions.
93 changes: 13 additions & 80 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,92 +43,25 @@ The following packages are required to use Systole:
* Pandas (>=0.24)
* Matplotlib (>=3.0.2)
* Seaborn (>=0.9.0)
* py-ecg-detectors (>=1.0.2)

Recording
=========

Systole natively supports the recording of PPG signals through the `Nonin 3012LP Xpod USB pulse oximeter <https://www.nonin.com/products/xpod/>`_ together with the `Nonin 8000SM 'soft-clip' fingertip sensors <https://www.nonin.com/products/8000s/>`_.
It can easily interface with `PsychoPy <https://www.psychopy.org/>`_ to record PPG signal during psychological experiments, and to synchronize stimulus deliver to e.g., systole or diastole.

For example, you can record and plot data in less than 6 lines of code:

.. code-block:: python
import serial
from systole.recording import Oximeter
ser = serial.Serial('COM4') # Add your USB port here
# Open serial port, initialize and plot recording for Oximeter
oxi = Oximeter(serial=ser).setup().read(duration=10)
Interfacing with PsychoPy
-------------------------

The ``Oximeter`` class can be used together with a stimulus presentation software to record cardiac activity during psychological experiments.

* The ``read()`` method

will record for a predefined amount of time (specified by the ``duration`` parameter, in seconds). This 'serial mode' is the easiest and most robust method, but it does not allow the execution of other instructions in the meantime.

.. code-block:: python
# Code 1 {}
oximeter.read(duration=10)
# Code 2 {}
* The ``readInWaiting()`` method
Interactive plotting functions and reports generation will also require the following packages to be installed:

will only read the bytes temporally stored in the USB buffer. For the Nonin device, this represents up to 10 seconds of recording (this procedure should be executed at least one time every 10 seconds for a continuous recording). When inserted into a while loop, it can record PPG signal in parallel with other commands.
* plotly (>=4.8.0)
* plotly_express (>=0.4.1)

.. code-block:: python
import time
tstart = time.time()
while time.time() - tstart < 10:
oximeter.readInWaiting()
# Insert code here {...}
Online detection
----------------

Online heart beat detection, for cardiac-stimulus synchrony:

.. code-block:: python
import serial
import time
from systole.recording import Oximeter
# Open serial port
ser = serial.Serial('COM4') # Change this value according to your setup
# Create an Oxymeter instance and initialize recording
oxi = Oximeter(serial=ser, sfreq=75, add_channels=4).setup()
For an overview of all the recording functionalities, you can refer to the following tutorials:

# Online peak detection for 10 seconds
tstart = time.time()
while time.time() - tstart < 10:
while oxi.serial.inWaiting() >= 5:
paquet = list(oxi.serial.read(5))
oxi.add_paquet(paquet[2]) # Add new data point
if oxi.peaks[-1] == 1:
print('Heartbeat detected')
* Recording
* Artefacts detection and artefacts correction
* Heart rate variability

Peaks detection
===============

Heartbeats can be detected in the PPG signal either online or offline.

Methods from clipping correction and peak detection algorithm is adapted from [#]_.

.. code-block:: python
# Plot data
oxi.plot_oximeter()
Recording
=========

.. figure:: https://github.com/embodied-computation-group/systole/raw/master/Images/recording.png
:align: center
Systole natively supports recording of physiological signals from the following setups:
* `Nonin 3012LP Xpod USB pulse oximeter <https://www.nonin.com/products/xpod/>`_ together with the `Nonin 8000SM 'soft-clip' fingertip sensors <https://www.nonin.com/products/8000s/>`_ (USB).
* Remote Data Access (RDA) via BrainVision Recorder together with Brain product ExG amplifier `<https://www.brainproducts.com/>`_ (Ethernet).

Artefact correction
===================
Expand Down
54 changes: 54 additions & 0 deletions examples/Tutorial_HRV.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
Recording
=========
"""

# Author: Nicolas Legrand <[email protected]>
# Licence: GPL v3

# It can easily interface with `PsychoPy <https://www.psychopy.org/>`_ 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 {...}
87 changes: 87 additions & 0 deletions examples/Tutorial_recording.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Recording PPG signal
====================
"""

# Author: Nicolas Legrand <[email protected]>
# 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
# <https://www.nonin.com/products/xpod/>`_ together with the `Nonin 8000SM
# 'soft-clip' fingertip sensors <https://www.nonin.com/products/8000s/>`_.
# This function can easily be integrated with other stimulus presentation
# software lie `PsychoPy <https://www.psychopy.org/>`_ to record cardiac
# activity during psychological experiments, or to synchronize stimulus
# delivery with cardiac phases (e.g. systole or diastole).


#%%
# Reading
# -------
# Recording and plotting your first time-series will only require 5 lines
# of code:

import serial
from systole.recording import Oximeter
ser = serial.Serial('COM4') # Add your USB port here

# Open serial port, initialize and plot recording for Oximeter
oxi = Oximeter(serial=ser).setup().read(duration=10)

# The signal can be directly plotted using built-in functions.
oxi.plot_oximeter()

##############################################################################
# .. figure:: https://github.com/embodied-computation-group/systole/raw/master/Images/recording.png
# :align: center
##############################################################################

#%%
# Interfacing with PsychoPy
# -------------------------

# * The ``read()`` method will record for a predefined amount of time
# (specified by the ``duration`` parameter, in seconds). This 'serial mode'
# is the easiest and most robust method, but it does not allow the execution
# of other instructions in the meantime.

# Code 1 {}
oximeter.read(duration=10)
# Code 2 {}

# * The ``readInWaiting()`` method will only read the bytes temporally stored
# in the USB buffer. For the Nonin device, this represents up to 10 seconds of
# recording (this procedure should be executed at least one time every 10
# seconds for a continuous recording). When inserted into a while loop, it can
# record PPG signal in parallel with other commands.

import time
tstart = time.time()
while time.time() - tstart < 10:
oximeter.readInWaiting()
# Insert code here {...}

#%%
# Online detection
# ----------------
# Online heart beat detection, for cardiac-stimulus synchrony

import serial
import time
from systole.recording import Oximeter

# Open serial port
ser = serial.Serial('COM4') # Change this value according to your setup

# Create an Oxymeter instance and initialize recording
oxi = Oximeter(serial=ser, sfreq=75, add_channels=4).setup()

# Online peak detection for 10 seconds
tstart = time.time()
while time.time() - tstart < 10:
while oxi.serial.inWaiting() >= 5:
paquet = list(oxi.serial.read(5))
oxi.add_paquet(paquet[2]) # Add new data point
if oxi.peaks[-1] == 1:
print('Heartbeat detected')
7 changes: 5 additions & 2 deletions examples/plot_ECGProcessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
# ---------------
# The peaks detection algorithms are imported from the py-ecg-detectors module:
# https://github.com/berndporr/py-ecg-detectors
signal, peaks = ecg_peaks(signal_df.ecg, method='hamilton', sfreq=2000,
signal, peaks = ecg_peaks(signal_df.ecg, method='hamilton', sfreq=1000,
find_local=True)

#%%
Expand All @@ -50,10 +50,12 @@
neutral[
np.round(np.where(signal_df.stim.to_numpy() == 3)[0]).astype(int)] = 1

#%%
# Event related plot
# ------------------
sns.set_context('talk')
fig, ax = plt.subplots(figsize=(8, 5))
for cond, col, data in zip(
for cond, data, col in zip(
['Neutral', 'Disgust'], [neutral, disgust],
[sns.xkcd_rgb["denim blue"], sns.xkcd_rgb["pale red"]]):

Expand All @@ -72,6 +74,7 @@
ax.set_ylabel('Heart Rate (BPM)')
ax.set_title('Instantaneous heart rate after neutral and disgusting images')
sns.despine()
plt.tight_layout()


#%%
Expand Down
3 changes: 3 additions & 0 deletions source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Detection
:toctree: generated/

oxi_peaks
ecg_peaks
rr_artefacts
interpolate_clipping

Expand Down Expand Up @@ -72,6 +73,7 @@ Recording
:toctree: generated/

recording.Oximeter
recording.BrainVisionExG

Utils
-----
Expand All @@ -86,3 +88,4 @@ Utils
heart_rate
to_angles
to_epochs
to_rr
15 changes: 9 additions & 6 deletions systole/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,16 @@ def oxi_peaks(x, sfreq=75, win=1, new_sfreq=1000, clipping=True,
>>> df = import_ppg() # Import PPG recording
>>> signal, peaks = oxi_peaks(df.ppg.to_numpy())
>>> print(f'{sum(peaks)} peaks detected.')
378 peaks detected.
References
----------
Some of the processing steps were adapted from the HeartPy toolbox:
https://python-heart-rate-analysis-toolkit.readthedocs.io/en/latest/index.html
.. [1] van Gent, P., Farah, H., van Nes, N. and van Arem, B., 2019.
Analysing Noisy Driver Physiology Real-Time Using Off-the-Shelf Sensors:
Heart Rate Analysis Software from the Taking the Fast Lane Project. Journal
of Open Research Software, 7(1), p.32. DOI: http://doi.org/10.5334/jors.241
"""

if isinstance(x, list):
x = np.asarray(x)

Expand Down Expand Up @@ -151,12 +150,14 @@ def ecg_peaks(x, sfreq=1000, new_sfreq=1000, method='pan-tompkins',
>>> signal, peaks = ecg_peaks(signal_df.ecg.to_numpy(), method='hamilton',
>>> sfreq=2000, find_local=True)
>>> print(f'{sum(peaks)} peaks detected.')
24 peaks detected.
References
----------
.. [#] Howell, L., Porr, B. Popular ECG R peak detectors written in
python. DOI: 10.5281/zenodo.3353396
"""

if isinstance(x, list):
x = np.asarray(x)

Expand Down Expand Up @@ -256,6 +257,8 @@ def rr_artefacts(rr, c1=0.13, c2=0.17, alpha=5.2):
>>> rr = simulate_rr() # Simulate RR time series
>>> artefacts = rr_artefacts(rr)
>>> print(artefacts.keys())
dict_keys(['subspace1', 'subspace2', 'subspace3', 'mRR', 'ectopic', 'long',
'short', 'missed', 'extra', 'threshold1', 'threshold2'])
References
----------
Expand Down Expand Up @@ -388,14 +391,14 @@ def interpolate_clipping(signal, threshold=255):
Examples
--------
.. plot::
>>> import matplotlib.pyplot as plt
>>> from systole import import_ppg
>>> from systole.detection import interpolate_clipping
>>> df = import_ppg()
>>> clean_signal = interpolate_clipping(df.ppg.to_numpy(),
>>> threshold=255)
>>> clean_signal = interpolate_clipping(df.ppg.to_numpy())
>>> plt.plot(df.time, clean_signal, color='#F15854')
>>> plt.plot(df.time, ppg, color='#5DA5DA')
>>> plt.plot(df.time, df.ppg, color='#5DA5DA')
>>> plt.axhline(y=255, linestyle='--', color='k')
>>> plt.xlabel('Time (s)')
>>> plt.ylabel('PPG level (a.u)')
Expand Down
Loading

0 comments on commit 07ad050

Please sign in to comment.