-
Notifications
You must be signed in to change notification settings - Fork 11
/
_export.py
147 lines (125 loc) · 4.9 KB
/
_export.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
"""Use pybv to export data from different software packages to BrainVision."""
from pathlib import Path
from warnings import warn
import numpy as np
from mne.channels.channels import _unit2human
from mne.io.constants import FIFF
from pybv import write_brainvision
def _export_mne_raw(*, raw, fname, events=None, overwrite=False):
"""Export raw data from MNE-Python.
Parameters
----------
raw : mne.io.Raw
The raw data to export.
fname : str | pathlib.Path
The name of the file where raw data will be exported to. Must end with
``".vhdr"``, and accompanying *.vmrk* and *.eeg* files will be written
inside the same directory.
events : np.ndarray | None
Events to be written to the marker file (*.vmrk*). When array, must be in
`MNE-Python format <https://mne.tools/stable/glossary.html#term-events>`_.
When ``None`` (default), events will be written based on ``raw.annotations``.
overwrite : bool
Whether or not to overwrite existing data. Defaults to ``False``.
"""
# Prepare file location
if not str(fname).endswith(".vhdr"):
raise ValueError("`fname` must have the '.vhdr' extension for BrainVision.")
fname = Path(fname)
folder_out = fname.parents[0]
fname_base = fname.stem
# Prepare data from raw
data = raw.get_data() # gets data starting from raw.first_samp
sfreq = raw.info["sfreq"] # in Hz
meas_date = raw.info["meas_date"] # datetime.datetime
ch_names = raw.ch_names
# write voltage units as micro-volts and all other units without
# scaling
# We write units that we don't know as n/a
unit = []
for ch in raw.info["chs"]:
if ch["unit"] == FIFF.FIFF_UNIT_V:
unit.append("µV")
elif ch["unit"] == FIFF.FIFF_UNIT_CEL:
unit.append("°C")
else:
unit.append(_unit2human.get(ch["unit"], "n/a"))
unit = [u if u != "NA" else "n/a" for u in unit]
# We enforce conversion to float32 format
# XXX: Could add a feature that checks data and optimizes `unit`, `resolution`,
# and `format` so that raw.orig_format could be retained if reasonable.
if raw.orig_format != "single":
warn(
f"Encountered data in '{raw.orig_format}' format. "
"Converting to float32.",
RuntimeWarning,
)
fmt = "binary_float32"
resolution = 0.1
# Handle events
# if we got an ndarray, this is in MNE-Python format
msg = "`events` must be None or array in MNE-Python format."
if events is not None:
# Subtract raw.first_samp because brainvision marks events starting from
# the first available data point and ignores the raw.first_samp
assert isinstance(events, np.ndarray), msg
assert events.ndim == 2, msg
assert events.shape[-1] == 3, msg
events[:, 0] -= raw.first_samp
events = events[:, [0, 2]] # reorder for pybv required order
# else, prepare pybv style events from raw.annotations
else:
events = _mne_annots2pybv_events(raw)
# no information about reference channels in mne currently
ref_ch_names = None
# write to BrainVision
write_brainvision(
data=data,
sfreq=sfreq,
ch_names=ch_names,
ref_ch_names=ref_ch_names,
fname_base=fname_base,
folder_out=folder_out,
overwrite=overwrite,
events=events,
resolution=resolution,
unit=unit,
fmt=fmt,
meas_date=meas_date,
)
def _mne_annots2pybv_events(raw):
"""Convert mne Annotations to pybv events."""
events = []
for annot in raw.annotations:
# Handle onset and duration: seconds to sample,
# relative to raw.first_samp / raw.first_time
onset = annot["onset"] - raw.first_time
onset = raw.time_as_index(onset).astype(int)[0]
duration = int(annot["duration"] * raw.info["sfreq"])
# Triage type and description
# Defaults to type="Comment", and the full description
etype = "Comment"
description = annot["description"]
for start in ["Stimulus/S", "Response/R", "Comment/"]:
if description.startswith(start):
etype = start.split("/")[0]
description = description.replace(start, "")
break
if etype in ["Stimulus", "Response"] and description.strip().isdigit():
description = int(description.strip())
else:
# if cannot convert to int, we must use this as "Comment"
etype = "Comment"
# Handle channels
channels = list(annot["ch_names"])
# Add a "pybv" event
events += [
dict(
onset=onset, # in samples
duration=duration, # in samples
description=description,
type=etype,
channels=channels,
)
]
return events