# -*- coding: utf-8 -*-
# Copyright (C) 2017-2023 Phillip Alday <me@phillipalday.com>
# License: BSD (3-clause)
"""File I/O utilities for EEG data."""
from __future__ import division, print_function
import codecs
import os
import mne
import numpy as np
from .. import __version__
# TODO: include boundaries in MNE annotation as segment markers
# in write_raw_brainvision
# TODO: expose support for different numeric formats
# TODO: support export to vectorized data
# TODO: allow arbitrary names for vmrk and eeg
# TODO: epochs exporter using segment markers
# TODO: epochs importer using segment markers
# (is there another epochs format?)
# TODO: make helper dicts private
# ascii as future formats
supported_formats = {
'binary_float32' : 'IEEE_FLOAT_32', # noqa: E203
'binary_int16' : 'INT_16', # noqa: E203
}
supported_orients = set(['multiplexed'])
[docs]def write_raw_brainvision(raw, vhdr_fname, events=True):
"""Write raw data to BrainVision format.
Parameters
----------
raw : instance of Raw
The raw data to do these estimations on.
vhdr_fname : str
Path to the EEG header file.
events : boolean or ndarray
If ndarry, events to write in marker file. Otherwise, boolean indicator
to extract and write events from raw.
Notes
-----
The BrainVision format supports by-channel filter and measurement
information and moreover distinguishes between hardware and software
filters. MNE does neither. Currently, filter information is not exported.
Moreover BrainVision also allows for more complex trigger codes than MNE's
simple integers, e.g. distinguishing on supported hardware between stimulus
codes (prefixed by an S) and responses codes (prefixed by an R). MNE's
numeric events are all treated as 'stimulus markers' and prefixed by an S
on output.
Note however that only channels of type 'eeg','eog', 'meg' and 'misc' are
exported. This follows from the observation that BrainVision recordings
produced by BrainProducts devices generally only contain EEG and a few
auxiliary channels. The stimulus channel is not exported as channel data,
but, in line with BrainVision convention, the events array can be exported
to the vmrk file. Channels marked as bad are also not exported, in line
with MNE's default behavior of generally ignoring bad channels. As the
current MNE readers do not do much with the channel-level annotations in
the vhdr file, it is not really desireable to depend on encoding
channel-type or "goodness" there. As such any information related to
channel-type or badness is lost upon export.
If you really want to export unsupported
datatypes or bad channels, then create a copy, mark everything as good and
of type 'eeg', and export. Be aware that the metadata will have to be
corrected the next time the data is read. Other options are to use the
private member functions directly that write each of the constituent files
(understanding that their API is not guaranteed to be stable) or use the
pybv library.
In other words, a round trip import-export is a lossy operation in terms of
metadata. The actual EEG recording should be losslessly preserved within
the realm of floating point precision and the constraints above.
"""
vmrk_fname = vhdr_fname[:-4] + 'vmrk'
eeg_fname = vhdr_fname[:-4] + 'eeg'
if isinstance(events, np.ndarray):
pass
elif events is False:
events = np.ndarray([0, 3])
elif events is True:
# if there are no events, then don't fail
try:
events = mne.find_events(raw, verbose=False)
except ValueError:
events = np.ndarray([0, 3])
else:
raise ValueError('events must be boolean or 3 x n_events ndarray.') # noqa: E501
# eliminate the stim channel
raw = raw.copy().pick_types(eeg=True, eog=True, meg=True, misc=True)
_write_vmrk_file(vmrk_fname, eeg_fname, events)
_write_vhdr_file(vhdr_fname, vmrk_fname, eeg_fname, raw)
_write_bveeg_file(eeg_fname, raw)
def _write_vmrk_file(vmrk_fname, eeg_fname, events):
"""Write BrainvVision marker file."""
with codecs.open(vmrk_fname, 'w', encoding='utf-8') as fout:
print(r'Brain Vision Data Exchange Marker File, Version 1.0', file=fout) # noqa: E501
print(r';Exported from MNE-Python using philistine {}'.format(__version__), file=fout) # noqa: E501
print(r'', file=fout)
print(r'[Common Infos]', file=fout)
print(r'Codepage=UTF-8', file=fout)
print(r'DataFile={}'.format(eeg_fname.split(os.sep)[-1]), file=fout) # noqa: E501
print(r'', file=fout)
print(r'[Marker Infos]', file=fout)
print(r'; Each entry: Mk<Marker number>=<Type>,<Description>,<Position in data points>,', file=fout) # noqa: E501
print(r'; <Size in data points>, <Channel number (0 = marker is related to all channels)>', file=fout) # noqa: E501
print(r'; Fields are delimited by commas, some fields might be omitted (empty).', file=fout) # noqa: E501
print(r'; Commas in type or description text are coded as "\1".', file=fout) # noqa: E501
print(r'Mk1=New Segment,,1,1,0,0', file=fout)
if events.shape[0] == 0:
return
twidth = int(np.ceil(np.log10(np.max(events[:, 2]))))
tformat = 'S{:>' + str(twidth) + '}'
for i, r in enumerate(range(events.shape[0]), start=2):
print(r'Mk{}=Stimulus,{},{},1,0'.format(i, tformat.format(events[r, 2]), # noqa: E501
events[r, 0]), file=fout)
def _write_vhdr_file(vhdr_fname, vmrk_fname, eeg_fname, raw,
orientation='multiplexed',
format='binary_float32'):
"""Write BrainvVision header file."""
fmt = format.lower()
if orientation.lower() not in supported_orients:
errmsg = ('Orientation {} not supported.'.format(orientation) +
'Currently supported orientations are: ' +
', '.join(supported_orients))
raise ValueError(errmsg)
if fmt not in supported_formats:
errmsg = ('Data format {} not supported.'.format(format) +
'Currently supported formats are: ' +
', '.join(supported_formats))
raise ValueError(errmsg)
with codecs.open(vhdr_fname, 'w', encoding='utf-8') as fout:
print(r'Brain Vision Data Exchange Header File Version 1.0', file=fout) # noqa: E501
print(r';Exported from MNE-Python using philistine {}'.format(__version__), file=fout) # noqa: E501
print(r'', file=fout)
print(r'[Common Infos]', file=fout)
print(r'Codepage=UTF-8', file=fout)
print(r'DataFile={}'.format(eeg_fname.split(os.sep)[-1]), file=fout) # noqa: E501
print(r'MarkerFile={}'.format(vmrk_fname.split(os.sep)[-1]), file=fout) # noqa: E501
if 'binary' in format.lower():
print(r'DataFormat=BINARY', file=fout)
if 'multiplexed' == orientation.lower():
print(r'Data orientation: MULTIPLEXED=ch1,pt1, ch2,pt1 ...', file=fout) # noqa: E501
print(r'DataOrientation=MULTIPLEXED', file=fout)
print(r'NumberOfChannels={}'.format(len(raw.ch_names)), file=fout) # noqa: E501
print(r'; Sampling interval in microseconds', file=fout)
print(r'SamplingInterval={}'.format(int(1e6 / raw.info['sfreq'])), file=fout) # noqa: E501
print(r'', file=fout)
if 'binary' in format.lower():
print(r'[Binary Infos]', file=fout)
print(r'BinaryFormat={}'.format(supported_formats[format]), file=fout) # noqa: E501
print(r'', file=fout)
print(r'[Channel Infos]', file=fout)
print(r'; Each entry: Ch<Channel number>=<Name>,<Reference channel name>,', file=fout) # noqa: E501
print(r'; <Resolution in microvolts>,<Future extensions..', file=fout)
print(r'; Fields are delimited by commas, some fields might be omitted (empty).', file=fout) # noqa: E501
print(r'; Commas in channel names are coded as "\1".', file=fout)
for i, ch in enumerate(raw.ch_names, start=1):
# not sure 0.1 µV is a sensible default resolution or if there is a
# good way to determine this based on the values in the array, but
# this is the resolution in the BV files this is being tested on
print(r'Ch{}={},,0.1'.format(i, ch), file=fout)
print(r'', file=fout)
print(r'[Comment]', file=fout)
print(r'', file=fout)
def _write_bveeg_file(eeg_fname, raw, orientation='multiplexed',
format='binary_float32'):
"""Write BrainVision data file."""
fmt = format.lower()
if orientation.lower() not in supported_orients:
errmsg = ('Orientation {} not supported.'.format(orientation) +
'Currently supported orientations are: ' +
', '.join(supported_orients))
raise ValueError(errmsg)
if fmt not in supported_formats:
errmsg = ('Data format {} not supported.'.format(format) +
'Currently supported formats are: ' +
', '.join(supported_formats))
raise ValueError(errmsg)
if fmt[:len('binary')] == 'binary':
dtype = np.dtype(format.lower()[len('binary') + 1:])
else:
errmsg = 'Cannot map data format {} to NumPy dtype'.format(format)
raise ValueError(errmsg)
# ndarry.tofile uses column-major order
# the multiplicative factor here is dependent on resolution
# for 0.1 µV, this works out to 1e7
# multiplexed:
# channel changes fast and channel is first axis -> C order
with open(eeg_fname, 'wb') as fout:
# skip the stim channel and scale
data = raw._data * 1e7
fout.write(data.astype(dtype=dtype).ravel(order='F').tobytes())
def _anonymize_bv(vmrk_fname):
"""Anonymize BrainVision marker files by stripping out time stamps."""
pass
def _rename_bv(vhdr_fname):
"""Rename a BrainVision file, including updating internal links."""
pass
def _extract_bv_segments(vmrk_fname):
"""Extract segments from BrainVision VMRK file."""
pass