Skip to content

Commit

Permalink
ENH: Add helper func to convert to MNE raw instance
Browse files Browse the repository at this point in the history
  • Loading branch information
scott-huberty committed Mar 25, 2024
1 parent d5aa079 commit 9dd276c
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 7 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The Eyelink Data Format (EDF; not to be confused with the [European Data Format]
Dependencies
============

Strictly speaking, EyeLinkIO only requires Numpy, and that the user has the [EyeLink Developers Kit](<https://www.sr-research.com/support/forum-3.html>) installed on their machine (One must create a login on the forum to access the download). We also plan to create helper functions for converting data to pandas `DataFrames` or MNE-Python Raw instances, after reading the data in. These functions would require the user to have those packages installed.
Strictly speaking, EyeLinkIO only requires Numpy, and that the user has the [EyeLink Developers Kit](<https://www.sr-research.com/support/forum-3.html>) installed on their machine (One must create a login on the forum to access the download). We also provide helper functions for converting data to pandas `DataFrames` or MNE-Python Raw instances, after reading the data in. These functions require the user to have those respective packages installed.

Example Usage
=============
Expand All @@ -29,7 +29,7 @@ print(edf_file)
```

```
<RawEDF | test_raw.edf>
<EDF | test_raw.edf>
Version: EYELINK II 1
Eye: RIGHT_EYE
Pupil unit: PUPIL_AREA
Expand All @@ -38,6 +38,12 @@ print(edf_file)
Length: 66.827 seconds
```

```
# Convert to a pandas DataFrame or an MNE Raw instance
dfs = edf_file.to_data_frame()
raw = edf_file.to_mne()
```

Acknowledgements
================

Expand Down
11 changes: 10 additions & 1 deletion eyelinkio/io/edf/read_edf.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
has_edfapi = False
why_not = str(exp)

from ...utils import to_data_frame
from ...utils import to_data_frame, to_mne
from . import _defines as defines
from ._defines import event_constants

Expand Down Expand Up @@ -106,6 +106,15 @@ def to_data_frame(self):
"""
return to_data_frame(self)

def to_mne(self):
"""Convert an EDF object to an MNE object.
Returns
-------
raw : :class:`mne.io.Raw`
"""
return to_mne(self)

class _edf_open:
"""Context manager for opening EDF files."""

Expand Down
13 changes: 13 additions & 0 deletions eyelinkio/io/tests/test_edf.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,16 @@ def test_to_data_frame():
assert all(isinstance(df, pd.DataFrame) for df in dfs.values())
np.testing.assert_equal(dfs["blinks"]["eye"].unique(), "LEFT_EYE")
assert dfs["messages"]["msg"][0] == "RECCFG CR 1000 2 1 L"

pytest.importorskip('mne')
def test_to_mne():
"""Test converting EDF to MNE."""
import mne

fname = _get_test_fnames()[0]
edf_file = read_edf(fname)
raw = edf_file.to_mne()
assert isinstance(raw, mne.io.RawArray)
assert raw.info["sfreq"] == edf_file["info"]["sfreq"]
tz = raw.info["meas_date"].tzinfo
assert raw.info["meas_date"] == edf_file["info"]["meas_date"].replace(tzinfo=tz)
2 changes: 1 addition & 1 deletion eyelinkio/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .utils import to_data_frame
from .utils import to_data_frame, to_mne
6 changes: 5 additions & 1 deletion eyelinkio/utils/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ def requires_edfapi(func):

def _check_pandas_installed(strict=True):
"""Aux function."""
return _soft_import("pandas", "dataframe integration", strict=strict)
return _soft_import("pandas", "converting to dataframes", strict=strict)

def _check_mne_installed(strict=True):
"""Aux function."""
return _soft_import("mne", "exporting to MNE", strict=strict)


def _soft_import(name, purpose, strict=True):
Expand Down
55 changes: 54 additions & 1 deletion eyelinkio/utils/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from datetime import timedelta, timezone
from pathlib import Path
from warnings import warn

from ..io.edf import _defines
from .check import _check_pandas_installed
from .check import _check_mne_installed, _check_pandas_installed


def _get_test_fnames():
Expand Down Expand Up @@ -58,3 +60,54 @@ def _convert_discrete_data(data, field_name):
else:
df["eye"] = (df["eye"]).map(_defines.eye_constants)
return df


def to_mne(edf_obj):
"""Convert an EDF object to an MNE object.
Parameters
----------
edf_obj : :class:`EDF`
The EDF object to convert to an MNE object.
Returns
-------
raw : :class:`mne.io.Raw
"""
mne = _check_mne_installed(strict=True)

# in mne we need to specify the eye in the ch name, or pick functions will fail
eye = edf_obj["info"]["eye"].split("_")[0].lower()
ch_names = edf_obj["info"]["sample_fields"]
ch_names = [f"{ch}_{eye}" for ch in ch_names]
ch_types = []
more_info = {}
for ch in ch_names:
if ch.startswith(("xpos", "ypos")):
ch_types.append("eyegaze")
if ch.startswith("x"):
more_info[f"{ch}"] = ("eyegaze", "px", f"{eye}", "x")
elif ch.startswith("y"):
more_info[f"{ch}"] = ("eyegaze", "px", f"{eye}", "y")
elif ch.startswith("ps"):
ch_types.append("pupil")
more_info[f"{ch}"] = ("pupil", "au", f"{eye}")
else:
warn(f"Unknown channel type: {ch}. Setting to misc.")
ch_types.append("misc")

# Create the info structure
info = mne.create_info(ch_names=ch_names,
sfreq=edf_obj["info"]["sfreq"],
ch_types=ch_types)
# force timezone to UTC
dt = edf_obj["info"]["meas_date"]
tz = timezone(timedelta(hours=0))
dt = dt.replace(tzinfo=tz)
info.set_meas_date(dt)

# Create the raw object
raw = mne.io.RawArray(edf_obj["samples"], info)
# This will set the loc array etc.
mne.preprocessing.eyetracking.set_channel_types_eyetrack(raw, more_info)
return raw
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ doc = [
"sphinx-design",
]

full = ["pandas"]
full = ["pandas", "mne"]
dev = ["eyelinkio[test,doc,full]"]

# [project.urls]
Expand Down

0 comments on commit 9dd276c

Please sign in to comment.