Skip to content

Commit

Permalink
initial commit:
Browse files Browse the repository at this point in the history
- create new raw class for et data recorded with SR research's eyelink system
- can read eyelink .asc files (using djangrew's ParseEyelinkAscFiles function - https://github.com/djangraw/ParseEyeLinkAscFiles/blob/master/ParseEyeLinkAsc.py)
- tbd: adding annotations, create info from file-header

work on parsed data integrity
- fill missing data in epoched recording with nan
- work on parsing errors
exclude rows that have negative time values

init testing framework

adapt higher level files to new class:
- add to mne.io.__init__.py
- add eyetrack as option to create_info() docstring

assert integrity of parsed data (not finished)

init annotation handling for asc reader (WIP)

lines too long in asc parser module

add meas_date from asc header

pep style

fix setting annotation times

handle sample intervals for different sample frequencies

linearly interpolate missing data, for plot functions etc to work.

add option to annotate missing data

move nan interpolation function to preprocessing.interpolation

fiff constants for new channel types

point test_constants to my fork of fiff-constants

set correct channel_type, loc, and coil_types

fix git target

update used fiff-constants

make plots work for ch_type='eyetrack'
adjust defaults for plotting

let test use mne-testing-data

optional reading of blinks/saccades from eyelink data file

add eyetrack also to the  instance.pick_types() method

add eyetracker units to fif-constants

function to find eyeblinks via nan values in eyetrack channels

handled error handling for file without blinks
  • Loading branch information
dominikwelke authored and scott-huberty committed Sep 9, 2022
1 parent 23a8da6 commit 5efcf53
Show file tree
Hide file tree
Showing 15 changed files with 705 additions and 21 deletions.
11 changes: 7 additions & 4 deletions mne/channels/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,8 +592,9 @@ def pick_types(self, meg=False, eeg=False, stim=False, eog=False,
ecg=False, emg=False, ref_meg='auto', misc=False,
resp=False, chpi=False, exci=False, ias=False, syst=False,
seeg=False, dipole=False, gof=False, bio=False,
ecog=False, fnirs=False, csd=False, dbs=False, include=(),
exclude='bads', selection=None, verbose=None):
ecog=False, fnirs=False, csd=False, dbs=False,
eyetrack=False,
include=(), exclude='bads', selection=None, verbose=None):
"""Pick some channels by type and names.
Parameters
Expand Down Expand Up @@ -648,6 +649,8 @@ def pick_types(self, meg=False, eeg=False, stim=False, eog=False,
EEG-CSD channels.
dbs : bool
Deep brain stimulation channels.
eyetrack : bool
Eye-tracking channels.
include : list of str
List of additional channels to include. If empty do not include
any.
Expand Down Expand Up @@ -675,8 +678,8 @@ def pick_types(self, meg=False, eeg=False, stim=False, eog=False,
self.info, meg=meg, eeg=eeg, stim=stim, eog=eog, ecg=ecg, emg=emg,
ref_meg=ref_meg, misc=misc, resp=resp, chpi=chpi, exci=exci,
ias=ias, syst=syst, seeg=seeg, dipole=dipole, gof=gof, bio=bio,
ecog=ecog, fnirs=fnirs, csd=csd, dbs=dbs, include=include,
exclude=exclude, selection=selection)
ecog=ecog, fnirs=fnirs, csd=csd, dbs=dbs, eyetrack=eyetrack,
include=include, exclude=exclude, selection=selection)

self._pick_drop_channels(idx)

Expand Down
19 changes: 13 additions & 6 deletions mne/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,29 @@
exci='k', ias='k', syst='k', seeg='saddlebrown', dbs='seagreen',
dipole='k', gof='k', bio='k', ecog='k', hbo='#AA3377', hbr='b',
fnirs_cw_amplitude='k', fnirs_fd_ac_amplitude='k',
fnirs_fd_phase='k', fnirs_od='k', csd='k', whitened='k'),
fnirs_fd_phase='k', fnirs_od='k', csd='k', whitened='k',
eyetrack='k'),
si_units=dict(mag='T', grad='T/m', eeg='V', eog='V', ecg='V', emg='V',
misc='AU', seeg='V', dbs='V', dipole='Am', gof='GOF',
bio='V', ecog='V', hbo='M', hbr='M', ref_meg='T',
fnirs_cw_amplitude='V', fnirs_fd_ac_amplitude='V',
fnirs_fd_phase='rad', fnirs_od='V', csd='V/m²',
whitened='Z'),
whitened='Z',
eyetrack='AU'),
units=dict(mag='fT', grad='fT/cm', eeg='µV', eog='µV', ecg='µV', emg='µV',
misc='AU', seeg='mV', dbs='µV', dipole='nAm', gof='GOF',
bio='µV', ecog='µV', hbo='µM', hbr='µM', ref_meg='fT',
fnirs_cw_amplitude='V', fnirs_fd_ac_amplitude='V',
fnirs_fd_phase='rad', fnirs_od='V', csd='mV/m²',
whitened='Z'),
whitened='Z',
eyetrack='AU'),
# scalings for the units
scalings=dict(mag=1e15, grad=1e13, eeg=1e6, eog=1e6, emg=1e6, ecg=1e6,
misc=1.0, seeg=1e3, dbs=1e6, ecog=1e6, dipole=1e9, gof=1.0,
bio=1e6, hbo=1e6, hbr=1e6, ref_meg=1e15,
fnirs_cw_amplitude=1.0, fnirs_fd_ac_amplitude=1.0,
fnirs_fd_phase=1., fnirs_od=1.0, csd=1e3, whitened=1.),
fnirs_fd_phase=1., fnirs_od=1.0, csd=1e3, whitened=1.,
eyetrack=1.),
# rough guess for a good plot
scalings_plot_raw=dict(mag=1e-12, grad=4e-11, eeg=20e-6, eog=150e-6,
ecg=5e-4, emg=1e-3, ref_meg=1e-12, misc='auto',
Expand All @@ -39,13 +43,15 @@
hbr=10e-6, whitened=10., fnirs_cw_amplitude=2e-2,
fnirs_fd_ac_amplitude=2e-2, fnirs_fd_phase=2e-1,
fnirs_od=2e-2, csd=200e-4,
dipole=1e-7, gof=1e2),
dipole=1e-7, gof=1e2,
eyetrack=1e3),
scalings_cov_rank=dict(mag=1e12, grad=1e11, eeg=1e5, # ~100x scalings
seeg=1e1, dbs=1e4, ecog=1e4, hbo=1e4, hbr=1e4),
ylim=dict(mag=(-600., 600.), grad=(-200., 200.), eeg=(-200., 200.),
misc=(-5., 5.), seeg=(-20., 20.), dbs=(-200., 200.),
dipole=(-100., 100.), gof=(0., 1.), bio=(-500., 500.),
ecog=(-200., 200.), hbo=(0, 20), hbr=(0, 20), csd=(-50., 50.)),
ecog=(-200., 200.), hbo=(0, 20), hbr=(0, 20), csd=(-50., 50.),
eyetrack=(0., 5000.)),
titles=dict(mag='Magnetometers', grad='Gradiometers', eeg='EEG', eog='EOG',
ecg='ECG', emg='EMG', misc='misc', seeg='sEEG', dbs='DBS',
bio='BIO', dipole='Dipole', ecog='ECoG', hbo='Oxyhemoglobin',
Expand All @@ -56,6 +62,7 @@
fnirs_od='fNIRS (OD)', hbr='Deoxyhemoglobin',
gof='Goodness of fit', csd='Current source density',
stim='Stimulus',
eyetrack='Eye-tracking',
),
mask_params=dict(marker='o',
markerfacecolor='w',
Expand Down
1 change: 1 addition & 0 deletions mne/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
read_evoked_fieldtrip)
from .nihon import read_raw_nihon
from ._read_raw import read_raw
from .eyetrack import read_raw_eyelink

# for backward compatibility
from .fiff import Raw
Expand Down
10 changes: 10 additions & 0 deletions mne/io/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
FIFF.FIFFV_DIPOLE_WAVE = 1000 # Dipole time curve (xplotter/xfit)
FIFF.FIFFV_GOODNESS_FIT = 1001 # Goodness of fit (xplotter/xfit)
FIFF.FIFFV_FNIRS_CH = 1100 # Functional near-infrared spectroscopy
FIFF.FIFFV_EYETRACK_CH = 1200 # Eye-tracking
_ch_kind_named = {key: key for key in (
FIFF.FIFFV_BIO_CH,
FIFF.FIFFV_MEG_CH,
Expand All @@ -223,6 +224,7 @@
FIFF.FIFFV_DIPOLE_WAVE,
FIFF.FIFFV_GOODNESS_FIT,
FIFF.FIFFV_FNIRS_CH,
FIFF.FIFFV_EYETRACK_CH,
)}

#
Expand Down Expand Up @@ -854,6 +856,9 @@
FIFF.FIFF_UNIT_AM = 202 # Am
FIFF.FIFF_UNIT_AM_M2 = 203 # Am/m^2
FIFF.FIFF_UNIT_AM_M3 = 204 # Am/m^3

FIFF.FIFF_UNIT_PX = 210 # Pixel
FIFF.FIFF_UNIT_DEG = 211 # Degrees of visual angle
_ch_unit_named = {key: key for key in(
FIFF.FIFF_UNIT_NONE, FIFF.FIFF_UNIT_UNITLESS, FIFF.FIFF_UNIT_M,
FIFF.FIFF_UNIT_KG, FIFF.FIFF_UNIT_SEC, FIFF.FIFF_UNIT_A, FIFF.FIFF_UNIT_K,
Expand All @@ -865,6 +870,7 @@
FIFF.FIFF_UNIT_CEL, FIFF.FIFF_UNIT_LM, FIFF.FIFF_UNIT_LX,
FIFF.FIFF_UNIT_V_M2, FIFF.FIFF_UNIT_T_M, FIFF.FIFF_UNIT_AM,
FIFF.FIFF_UNIT_AM_M2, FIFF.FIFF_UNIT_AM_M3,
FIFF.FIFF_UNIT_PX, FIFF.FIFF_UNIT_DEG,
)}
#
# Multipliers
Expand Down Expand Up @@ -917,6 +923,9 @@
FIFF.FIFFV_COIL_FNIRS_FD_PHASE = 305 # fNIRS frequency domain phase
FIFF.FIFFV_COIL_FNIRS_RAW = FIFF.FIFFV_COIL_FNIRS_CW_AMPLITUDE # old alias

FIFF.FIFFV_COIL_EYETRACK_POS = 400 # Eye-tracking gaze position
FIFF.FIFFV_COIL_EYETRACK_PUPIL = 401 # Eye-tracking pupil size

FIFF.FIFFV_COIL_MCG_42 = 1000 # For testing the MCG software

FIFF.FIFFV_COIL_POINT_MAGNETOMETER = 2000 # Simple point magnetometer
Expand Down Expand Up @@ -1003,6 +1012,7 @@
FIFF.FIFFV_COIL_FNIRS_HBR, FIFF.FIFFV_COIL_FNIRS_RAW,
FIFF.FIFFV_COIL_FNIRS_OD, FIFF.FIFFV_COIL_FNIRS_FD_AC_AMPLITUDE,
FIFF.FIFFV_COIL_FNIRS_FD_PHASE, FIFF.FIFFV_COIL_MCG_42,
FIFF.FIFFV_COIL_EYETRACK_POS, FIFF.FIFFV_COIL_EYETRACK_PUPIL,
FIFF.FIFFV_COIL_POINT_MAGNETOMETER, FIFF.FIFFV_COIL_AXIAL_GRAD_5CM,
FIFF.FIFFV_COIL_VV_PLANAR_W, FIFF.FIFFV_COIL_VV_PLANAR_T1,
FIFF.FIFFV_COIL_VV_PLANAR_T2, FIFF.FIFFV_COIL_VV_PLANAR_T3,
Expand Down
186 changes: 186 additions & 0 deletions mne/io/eyetrack/ParseEyeLinkAscFiles_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# ParseEyeLinkAsc.py
# - Reads in .asc data files from EyeLink and produces pandas dataframes for
# further analysis
#
# Created 7/31/18-8/15/18 by DJ.
# Updated 7/4/19 by DJ - detects and handles monocular sample data.


def ParseEyeLinkAsc_(elFilename):
# dfRec,dfMsg,dfFix,dfSacc,dfBlink,dfSamples = ParseEyeLinkAsc(elFilename)
# -Reads in data files from EyeLink .asc file and produces readable
# dataframes for further analysis.
#
# INPUTS:
# -elFilename is a string indicating an EyeLink data file from an AX-CPT
# task in the current path.
#
# OUTPUTS:
# -dfRec contains information about recording periods (often trials)
# -dfMsg contains information about messages (usually sent from stimulus
# software)
# -dfFix contains information about fixations
# -dfSacc contains information about saccades
# -dfBlink contains information about blinks
# -dfSamples contains information about individual samples
#
# Created 7/31/18-8/15/18 by DJ.
# Updated 11/12/18 by DJ - switched from "trials" to "recording periods"
# for experiments with continuous recording
# Updated 9/??/19 by Dominik Welke - fixed read-in of data

# Import packages
import numpy as np
import pandas as pd
import time

# ===== READ IN FILES ===== #
# Read in EyeLink file
print('Reading in EyeLink file %s...' % elFilename)
t = time.time()
with open(elFilename, "r+") as f:
fileTxt0 = (line.rstrip() for line in f)
# fileTxt0 = [line for line in fileTxt0 if line] # Non-blank lines in
# a list
fileTxt0 = [line for line in fileTxt0] # lines in a list
fileTxt0 = np.array(fileTxt0)

print('Done! Took %f seconds.' % (time.time() - t))

# Separate lines into samples and messages
print('Sorting lines...')
nLines = len(fileTxt0)
lineType = np.array(['OTHER'] * nLines, dtype='object')
# iStartRec = None
iStartRec = [] # DW: make a list of all rec-starts
t = time.time()
for iLine in range(nLines):
if len(fileTxt0[iLine]) < 3:
lineType[iLine] = 'EMPTY'
elif (
fileTxt0[iLine].startswith('*') or
fileTxt0[iLine].startswith('>>>>>')):
lineType[iLine] = 'COMMENT'
elif (
fileTxt0[iLine].split()[0][0].isdigit() or
fileTxt0[iLine].split()[0].startswith('-')):
lineType[iLine] = 'SAMPLE'
else:
lineType[iLine] = fileTxt0[iLine].split()[0]
# TODO: Find more general way of determining if recording has started
# if '!CAL' in fileTxt0[iLine]:
# iStartRec = iLine + 1
if 'START' in fileTxt0[iLine]:
# DW: more general way of determining if recording has started..
iStartRec.append(iLine + 1)
print('Done! Took %f seconds.' % (time.time() - t))

# ===== PARSE EYELINK FILE ===== #
t = time.time()
# Trials
print('Parsing recording markers...')
iNotStart = np.nonzero(lineType != 'START')[0]
dfRecStart = pd.read_csv(elFilename, skiprows=iNotStart, header=None,
delim_whitespace=True, usecols=[1])
dfRecStart.columns = ['tStart']
iNotEnd = np.nonzero(lineType != 'END')[0]
dfRecEnd = pd.read_csv(elFilename, skiprows=iNotEnd, header=None,
delim_whitespace=True, usecols=[1, 5, 6])
dfRecEnd.columns = ['tEnd', 'xRes', 'yRes']
# combine trial info
dfRec = pd.concat([dfRecStart, dfRecEnd], axis=1)
nRec = dfRec.shape[0]
print('%d recording periods found.' % nRec)

# Import Messages
print('Parsing stimulus messages...')
t = time.time()
iMsg = np.nonzero(lineType == 'MSG')[0]
# set up
tMsg = []
txtMsg = []
t = time.time()
for i in range(len(iMsg)):
# separate MSG prefix and timestamp from rest of message
info = fileTxt0[iMsg[i]].split()
# extract info
tMsg.append(int(info[1]))
txtMsg.append(' '.join(info[2:]))
# Convert dict to dataframe
dfMsg = pd.DataFrame({'time': tMsg, 'text': txtMsg})
print('Done! Took %f seconds.' % (time.time() - t))

# Import Fixations
print('Parsing fixations...')
t = time.time()
iNotEfix = np.nonzero(lineType != 'EFIX')[0]
dfFix = pd.read_csv(elFilename, skiprows=iNotEfix, header=None,
delim_whitespace=True, usecols=range(1, 8))
dfFix.columns = ['eye', 'tStart', 'tEnd', 'duration',
'xAvg', 'yAvg', 'pupilAvg']
nFix = dfFix.shape[0]
print('Done! Took %f seconds.' % (time.time() - t))

# Saccades
print('Parsing saccades...')
t = time.time()
iNotEsacc = np.nonzero(lineType != 'ESACC')[0]
dfSacc = pd.read_csv(elFilename, skiprows=iNotEsacc, header=None,
delim_whitespace=True, usecols=range(1, 11))
dfSacc.columns = ['eye', 'tStart', 'tEnd', 'duration', 'xStart',
'yStart', 'xEnd', 'yEnd', 'ampDeg', 'vPeak']
print('Done! Took %f seconds.' % (time.time() - t))

# Blinks
print('Parsing blinks...')
iNotEblink = np.nonzero(lineType != 'EBLINK')[0]
try:
dfBlink = pd.read_csv(elFilename, skiprows=iNotEblink, header=None,
delim_whitespace=True, usecols=range(1, 5))
except ValueError as this_error:
if str(this_error) == 'No columns to parse from file':
print('No blinks in file')
dfBlink = pd.DataFrame(columns=['eye','tStart','tEnd','duration'])
else:
raise
dfBlink.columns = ['eye', 'tStart', 'tEnd', 'duration']
print('Done! Took %f seconds.' % (time.time() - t))

# determine sample columns based on eyes recorded in file
eyesInFile = np.unique(dfFix.eye)
if eyesInFile.size == 2:
print('binocular data detected.')
cols = ['tSample', 'LX', 'LY', 'LPupil', 'RX', 'RY', 'RPupil']
else:
eye = eyesInFile[0]
print('monocular data detected (%c eye).' % eye)
cols = ['tSample', '%cX' % eye, '%cY' % eye, '%cPupil' % eye]
# Import samples
print('Parsing samples...')
t = time.time()
# iNotSample = np.nonzero(np.logical_or(
# lineType != 'SAMPLE', np.arange(nLines) < iStartRec))[0]
iNotSample = np.nonzero( # DW: try this, to get ALL data
np.logical_or(
lineType != 'SAMPLE', np.arange(nLines) < iStartRec[0]))[0]
dfSamples = pd.read_csv(elFilename, skiprows=iNotSample, header=None,
delim_whitespace=True, usecols=range(0, len(cols)))
dfSamples.columns = cols
# Convert values to numbers
for eye in ['L', 'R']:
if eye in eyesInFile:
dfSamples['%cX' % eye] = pd.to_numeric(dfSamples['%cX' % eye],
errors='coerce')
dfSamples['%cY' % eye] = pd.to_numeric(dfSamples['%cY' % eye],
errors='coerce')
dfSamples['%cPupil' % eye] = pd.to_numeric(
dfSamples['%cPupil' % eye], errors='coerce')
else:
dfSamples['%cX' % eye] = np.nan
dfSamples['%cY' % eye] = np.nan
dfSamples['%cPupil' % eye] = np.nan

print('Done! Took %.1f seconds.' % (time.time() - t))

# Return new compilation dataframe
return dfRec, dfMsg, dfFix, dfSacc, dfBlink, dfSamples
8 changes: 8 additions & 0 deletions mne/io/eyetrack/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Module for loading Eye-Tracker data"""

# Author: Dominik Welke <[email protected]>
#
# License: BSD-3-Clause

from .eyetrack import read_raw_eyelink
from .ParseEyeLinkAscFiles_ import ParseEyeLinkAsc_
Loading

0 comments on commit 5efcf53

Please sign in to comment.