Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use edfio for reading and writing edf files #195

Merged
merged 7 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- uses: actions/setup-python@v4
name: Install Python
with:
python-version: 3.8
python-version: 3.9

- name: Build sdist
run: |
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ If another PR is merged while you are working on something, a merge conflict may


## Development environment
Make sure to use Python 3.8. You might want to [create a virtual environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments) instead of working your main environment. In the root of the local clone of your fork, install SleepECG as follows:
Make sure to use Python 3.9. You might want to [create a virtual environment](https://docs.python.org/3/library/venv.html#creating-virtual-environments) instead of working your main environment. In the root of the local clone of your fork, install SleepECG as follows:

```
pip install -e ".[dev]"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ beats = detect_heartbeats(ecg, fs) # indices of detected heartbeats

### Dependencies

SleepECG requires Python ≥ 3.8 and the following packages:
SleepECG requires Python ≥ 3.9 and the following packages:

- [numpy](https://numpy.org/) ≥ 1.20.0
- [requests](https://requests.readthedocs.io/en/latest/) ≥ 2.25.0
Expand All @@ -73,9 +73,9 @@ SleepECG requires Python ≥ 3.8 and the following packages:

Optional dependencies provide additional features:

- [edfio](https://github.com/the-siesta-group/edfio/) ≥ 0.1.1 (read data from [MESA](https://sleepdata.org/datasets/mesa) and [SHHS](https://sleepdata.org/datasets/shhs))
- [joblib](https://joblib.readthedocs.io/en/latest/) ≥ 1.0.0 (parallelized feature extraction)
- [matplotlib](https://matplotlib.org/) ≥ 3.5.0 (plot ECG time courses, hypnograms, and confusion matrices)
- [mne](https://mne.tools/stable/index.html) ≥ 1.0.0 (read data from [MESA](https://sleepdata.org/datasets/mesa) and [SHHS](https://sleepdata.org/datasets/shhs))
- [numba](https://numba.pydata.org/) ≥ 0.55.0 (JIT-compiled heartbeat detector)
- [tensorflow](https://www.tensorflow.org/) ≥ 2.7.0 (sleep stage classification with Keras models)
- [wfdb](https://github.com/MIT-LCP/wfdb-python/) ≥ 3.4.0 (read data from [SLPDB](https://physionet.org/content/slpdb), [MITDB](https://physionet.org/content/mitdb), and [LTDB](https://physionet.org/content/ltdb))
Expand Down
24 changes: 12 additions & 12 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,27 +86,27 @@ plt.show()

## Sleep staging custom data

This example requires `mne` and `tensorflow` packages. In addition, it uses an example file [`sleep.edf`](https://osf.io/download/mx7av/), which contains ECG data for a whole night. Download and save this file in your working directory before running this example.
This example requires `edfio` and `tensorflow` packages. In addition, it uses an example file [`sleep.edf`](https://osf.io/download/mx7av/), which contains ECG data for a whole night. Download and save this file in your working directory before running this example.

```python
from datetime import datetime, timezone
from datetime import datetime

from mne.io import read_raw_edf
from edfio import read_edf

import sleepecg

# load dataset
raw = read_raw_edf("sleep.edf", include="ECG")
raw.set_channel_types({"ECG": "ecg"})
fs = raw.info["sfreq"]
edf = read_edf("sleep.edf")

# crop dataset (we only want data for the sleep duration)
start = datetime(2023, 3, 1, 23, 0, 0, tzinfo=timezone.utc)
stop = datetime(2023, 3, 2, 6, 0, 0, tzinfo=timezone.utc)
raw.crop((start - raw.info["meas_date"]).seconds, (stop - raw.info["meas_date"]).seconds)

# get ECG time series as 1D NumPy array
ecg = raw.get_data().squeeze()
start = datetime(2023, 3, 1, 23, 0, 0)
stop = datetime(2023, 3, 2, 6, 0, 0)
rec_start = datetime.combine(edf.startdate, edf.starttime)
edf.slice_between_seconds((start - rec_start).seconds, (stop - rec_start).seconds)

# get ECG time series and sampling freqeuncy
hofaflo marked this conversation as resolved.
Show resolved Hide resolved
ecg = edf.get_signal("ECG").data
fs = edf.get_signal("ECG").sampling_frequency

# detect heartbeats
beats = sleepecg.detect_heartbeats(ecg, fs)
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Documentation for SleepECG is available on [Read the Docs](https://sleepecg.read
Check out the [changelog](https://github.com/cbrnr/sleepecg/blob/main/CHANGELOG.md) to learn what we added, changed, or fixed.

## Dependencies
SleepECG requires Python ≥ 3.8 and the following packages:
SleepECG requires Python ≥ 3.9 and the following packages:

- [numpy](https://numpy.org/) ≥ 1.20.0
- [requests](https://requests.readthedocs.io/en/latest/) ≥ 2.25.0
Expand All @@ -22,9 +22,9 @@ SleepECG requires Python ≥ 3.8 and the following packages:

Optional dependencies provide additional features:

- [edfio](https://github.com/the-siesta-group/edfio/) ≥ 0.1.1 (read data from [MESA](https://sleepdata.org/datasets/mesa) and [SHHS](https://sleepdata.org/datasets/shhs))
- [joblib](https://joblib.readthedocs.io/en/latest/) ≥ 1.0.0 (parallelized feature extraction)
- [matplotlib](https://matplotlib.org/) ≥ 3.5.0 (plot ECG time courses, hypnograms, and confusion matrices)
- [mne](https://mne.tools/stable/index.html) ≥ 1.0.0 (read data from [MESA](https://sleepdata.org/datasets/mesa) and [SHHS](https://sleepdata.org/datasets/shhs))
- [numba](https://numba.pydata.org/) ≥ 0.55.0 (JIT-compiled heartbeat detector)
- [tensorflow](https://www.tensorflow.org/) ≥ 2.7.0 (sleep stage classification with Keras models)
- [wfdb](https://github.com/MIT-LCP/wfdb-python/) ≥ 3.4.0 (read data from [SLPDB](https://physionet.org/content/slpdb), [MITDB](https://physionet.org/content/mitdb), and [LTDB](https://physionet.org/content/ltdb))
Expand Down
12 changes: 4 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ authors = [
{name = "Florian Hofer", email = "[email protected]"},
{name = "Clemens Brunner", email = "[email protected]"},
]
requires-python = ">=3.8"
requires-python = ">=3.9"
classifiers = [
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand All @@ -33,25 +32,24 @@ dynamic = ["version", "readme"]

[project.optional-dependencies]
full = [ # complete package functionality
"edfio >= 0.1.1",
"joblib >= 1.0.0",
"matplotlib >= 3.5.0",
"mne >= 1.0.0",
"numba >= 0.55.0; python_version < '3.12'",
"tensorflow >= 2.7.0; python_version < '3.12'",
"wfdb >= 3.4.0",
]

dev = [ # everything needed for development
"black >= 22.6.0",
"edfio >= 0.1.1",
"joblib >= 1.0.0",
"matplotlib >= 3.5.0",
"mkdocs-material >= 8.4.0",
"mkdocstrings-python >= 0.7.1",
"mne >= 1.0.0",
"mypy >= 0.991",
"numba >= 0.55.0; python_version < '3.12'",
"pre-commit >= 2.13.0",
"pyEDFlib >= 0.1.25",
"pytest >= 6.2.0",
"ruff >= 0.0.263",
"setuptools >= 56.0.0",
Expand All @@ -65,9 +63,8 @@ doc = [ # RTD uses this when building the documentation
]

cibw = [ # cibuildwheel uses this for running the test suite
"mne >= 1.0.0",
"edfio >= 0.1.1",
"numba >= 0.55.0; python_version < '3.12'",
"pyEDFlib >= 0.1.25",
"wfdb >= 3.4.0",
]

Expand Down Expand Up @@ -109,7 +106,6 @@ pretty = true
markers = ["c_extension"]
filterwarnings = [
"error",
'ignore:.*sample_rate:DeprecationWarning:pyedflib',
'ignore:.*pkg_resources:DeprecationWarning',
'ignore:.*datetime.datetime.utcfromtimestamp\(\):DeprecationWarning',
]
Expand Down
20 changes: 8 additions & 12 deletions sleepecg/io/sleep_readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def read_mesa(
SleepRecord
Each element in the generator is of type `SleepRecord`.
"""
from mne.io import read_raw_edf
from edfio import read_edf

DB_SLUG = "mesa"
ANNOTATION_DIRNAME = "polysomnography/annotations-events-nsrr"
Expand Down Expand Up @@ -330,11 +330,9 @@ def read_mesa(
checksums[edf_filename],
)

rec = read_raw_edf(edf_filepath, verbose=False)
ecg = rec.get_data("EKG").ravel()
fs = rec.info["sfreq"]
heartbeat_indices = detect_heartbeats(ecg, fs)
heartbeat_times = heartbeat_indices / fs
ecg = read_edf(edf_filepath).get_signal("EKG")
heartbeat_indices = detect_heartbeats(ecg.data, ecg.sampling_frequency)
heartbeat_times = heartbeat_indices / ecg.sampling_frequency
np.save(heartbeats_file, heartbeat_times)

if not edf_was_available and not keep_edfs:
Expand Down Expand Up @@ -513,7 +511,7 @@ def read_shhs(
SleepRecord
Each element in the generator is of type `SleepRecord`.
"""
from mne.io import read_raw_edf
from edfio import read_edf

DB_SLUG = "shhs"
ANNOTATION_DIRNAME = "polysomnography/annotations-events-nsrr"
Expand Down Expand Up @@ -643,11 +641,9 @@ def read_shhs(
checksums[edf_filename],
)

rec = read_raw_edf(edf_filepath, verbose=False)
ecg = rec.get_data("ECG").ravel()
fs = rec.info["sfreq"]
heartbeat_indices = detect_heartbeats(ecg, fs)
heartbeat_times = heartbeat_indices / fs
ecg = read_edf(edf_filepath).get_signal("ECG")
heartbeat_indices = detect_heartbeats(ecg.data, ecg.sampling_frequency)
heartbeat_times = heartbeat_indices / ecg.sampling_frequency

heartbeats_file.parent.mkdir(parents=True, exist_ok=True)
np.save(heartbeats_file, heartbeat_times)
Expand Down
7 changes: 3 additions & 4 deletions sleepecg/test/test_sleep_readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
from pathlib import Path

import numpy as np
from edfio import Edf, EdfSignal

try:
from scipy.datasets import electrocardiogram # SciPy ≥ 1.10
except ImportError:
from scipy.misc import electrocardiogram # SciPy < 1.10
from pyedflib import highlevel

from sleepecg import SleepStage, read_mesa, read_shhs, read_slpdb
from sleepecg.io.sleep_readers import Gender
Expand All @@ -25,9 +25,8 @@ def _dummy_nsrr_edf(filename: str, hours: float, ecg_channel: str):
ECG_FS = 360
ecg_5_min = electrocardiogram()
seconds = int(hours * 60 * 60)
ecg = np.tile(ecg_5_min, int(np.ceil(seconds / 300)))[np.newaxis, : seconds * ECG_FS]
signal_headers = highlevel.make_signal_headers([ecg_channel], sample_frequency=ECG_FS)
highlevel.write_edf(filename, ecg, signal_headers)
ecg = np.tile(ecg_5_min, int(np.ceil(seconds / 300)))[: seconds * ECG_FS]
Edf([EdfSignal(ecg, ECG_FS, label=ecg_channel)]).write(filename)


def _dummy_nsrr_xml(filename: str, hours: float, random_state: int):
Expand Down
Loading