-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MRG, ENH: Export evokeds MFF (#9406)
* ENH: Add export evokeds to MFF function * ENH: Add export evokeds function * Add tests for export evokeds * DOC: Add export_evokeds to python reference * DOC: Fix history argument type * STY: Use logger for user message * FIX: Nest pytz import * Clarify MFF specific arguments * DOC: Use shared warning message * FIX: Test digitization data from categories.xml * STY: Use regex match for pytest.raises * STY: Use zipped lists * Refactor assert statements * ENH: Store MFF device type in info * Determine device type from info * Use kwargs for format-specific exports * Use current time for record time Because averaged files are absolute time agnostic, we can simplify by using plugging in the current time for the record time. This is also the behavior when running an averaging tool in EGI Net Station software. * Add MFF export funtion to python reference * ENH: Reorganize * FIX: Missed a file * ENH: For newer mffpy Co-authored-by: Eric Larson <[email protected]>
- Loading branch information
1 parent
dbfaeac
commit 8702d0e
Showing
11 changed files
with
323 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,4 +14,6 @@ Exporting | |
:toctree: generated/ | ||
|
||
export_epochs | ||
export_evokeds | ||
export_evokeds_mff | ||
export_raw |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
from ._export import export_raw, export_epochs | ||
from ._export import export_raw, export_epochs, export_evokeds | ||
from ._egimff import export_evokeds_mff |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
# -*- coding: utf-8 -*- | ||
# Authors: MNE Developers | ||
# | ||
# License: BSD (3-clause) | ||
|
||
import datetime | ||
|
||
import numpy as np | ||
|
||
from ..io.egi.egimff import _import_mffpy | ||
from ..io.pick import pick_types, pick_channels | ||
from ..utils import verbose | ||
|
||
|
||
@verbose | ||
def export_evokeds_mff(fname, evoked, history=None, *, verbose=None): | ||
"""Export evoked dataset to MFF. | ||
Parameters | ||
---------- | ||
%(export_params_fname)s | ||
evoked : list of Evoked instances | ||
List of evoked datasets to export to one file. Note that the | ||
measurement info from the first evoked instance is used, so be sure | ||
that information matches. | ||
history : None (default) | list of dict | ||
Optional list of history entries (dictionaries) to be written to | ||
history.xml. This must adhere to the format described in | ||
mffpy.xml_files.History.content. If None, no history.xml will be | ||
written. | ||
%(verbose)s | ||
Notes | ||
----- | ||
.. versionadded:: 0.24 | ||
Only EEG channels are written to the output file. | ||
``info['device_info']['type']`` must be a valid MFF recording device | ||
(e.g. 'HydroCel GSN 256 1.0'). This field is automatically populated when | ||
using MFF read functions. | ||
""" | ||
mffpy = _import_mffpy('Export evokeds to MFF.') | ||
import pytz | ||
info = evoked[0].info | ||
if np.round(info['sfreq']) != info['sfreq']: | ||
raise ValueError('Sampling frequency must be a whole number. ' | ||
f'sfreq: {info["sfreq"]}') | ||
sampling_rate = int(info['sfreq']) | ||
|
||
# Initialize writer | ||
writer = mffpy.Writer(fname) | ||
current_time = pytz.utc.localize(datetime.datetime.utcnow()) | ||
writer.addxml('fileInfo', recordTime=current_time) | ||
try: | ||
device = info['device_info']['type'] | ||
except (TypeError, KeyError): | ||
raise ValueError('No device type. Cannot determine sensor layout.') | ||
writer.add_coordinates_and_sensor_layout(device) | ||
|
||
# Add EEG data | ||
eeg_channels = pick_types(info, eeg=True, exclude=[]) | ||
eeg_bin = mffpy.bin_writer.BinWriter(sampling_rate) | ||
for ave in evoked: | ||
# Signals are converted to µV | ||
block = (ave.data[eeg_channels] * 1e6).astype(np.float32) | ||
eeg_bin.add_block(block, offset_us=0) | ||
writer.addbin(eeg_bin) | ||
|
||
# Add categories | ||
categories_content = _categories_content_from_evokeds(evoked) | ||
writer.addxml('categories', categories=categories_content) | ||
|
||
# Add history | ||
if history: | ||
writer.addxml('historyEntries', entries=history) | ||
|
||
writer.write() | ||
|
||
|
||
def _categories_content_from_evokeds(evoked): | ||
"""Return categories.xml content for evoked dataset.""" | ||
content = dict() | ||
begin_time = 0 | ||
for ave in evoked: | ||
# Times are converted to microseconds | ||
sfreq = ave.info['sfreq'] | ||
duration = np.round(len(ave.times) / sfreq * 1e6).astype(int) | ||
end_time = begin_time + duration | ||
event_time = begin_time - np.round(ave.tmin * 1e6).astype(int) | ||
eeg_bads = _get_bad_eeg_channels(ave.info) | ||
content[ave.comment] = [ | ||
_build_segment_content(begin_time, end_time, event_time, eeg_bads, | ||
name='Average', nsegs=ave.nave) | ||
] | ||
begin_time += duration | ||
return content | ||
|
||
|
||
def _get_bad_eeg_channels(info): | ||
"""Return a list of bad EEG channels formatted for categories.xml. | ||
Given a list of only the EEG channels in file, return the indices of this | ||
list (starting at 1) that correspond to bad channels. | ||
""" | ||
if len(info['bads']) == 0: | ||
return [] | ||
eeg_channels = pick_types(info, eeg=True, exclude=[]) | ||
bad_channels = pick_channels(info['ch_names'], info['bads']) | ||
bads_elementwise = np.isin(eeg_channels, bad_channels) | ||
return list(np.flatnonzero(bads_elementwise) + 1) | ||
|
||
|
||
def _build_segment_content(begin_time, end_time, event_time, eeg_bads, | ||
status='unedited', name=None, pns_bads=None, | ||
nsegs=None): | ||
"""Build content for a single segment in categories.xml. | ||
Segments are sorted into categories in categories.xml. In a segmented MFF | ||
each category can contain multiple segments, but in an averaged MFF each | ||
category only contains one segment (the average). | ||
""" | ||
channel_status = [{ | ||
'signalBin': 1, | ||
'exclusion': 'badChannels', | ||
'channels': eeg_bads | ||
}] | ||
if pns_bads: | ||
channel_status.append({ | ||
'signalBin': 2, | ||
'exclusion': 'badChannels', | ||
'channels': pns_bads | ||
}) | ||
content = { | ||
'status': status, | ||
'beginTime': begin_time, | ||
'endTime': end_time, | ||
'evtBegin': event_time, | ||
'evtEnd': event_time, | ||
'channelStatus': channel_status, | ||
} | ||
if name: | ||
content['name'] = name | ||
if nsegs: | ||
content['keys'] = { | ||
'#seg': { | ||
'type': 'long', | ||
'data': nsegs | ||
} | ||
} | ||
return content |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.