Skip to content

Commit 6e8709d

Browse files
committed
xdf tutorial
1 parent 82f6042 commit 6e8709d

6 files changed

Lines changed: 500 additions & 315 deletions

File tree

hypyp/xdf/xdf_import.py

Lines changed: 105 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@
88

99
class XDFImport():
1010
"""
11-
Read an XDF file and enable to export stream in a convenient format (e.g., an EEG stream into an mne.Raw instance).
11+
Read an XDF file and (optionally) create mne Raws and Annotations
1212
13-
Arguments:
13+
Parameters:
1414
file_path: Path to XDF file (LSL data recorded with LabRecorder). Can be absolute or relative.
15-
stream_type: Define which type of stream the user is looking to convert.
16-
stream_matches: List of the stream index(es) in the XDF the user wishes to convert (can be `str` which the class will try to match to the name of an existing stream or an `int` which will be interpreted as such). Do not set to convert all of the request type
15+
select_type: Define which type of stream the user is looking to convert. Should match the stream type in LSL
16+
select_matches: List of the stream index(es) in the XDF the user wishes to convert (can be `str` which the class will try to match to the name of an existing stream or an `int` which will be interpreted as such)
1717
mne_type_map: Dict to map stream types to mne channel types
1818
scale: Scaling factor or 'auto' for automatic scaling, None for no scaling.
19+
convert_to_mne: Flag to disable the automatic conversion to mne Raws and Annotations
20+
verbose: Verbose flag
1921
"""
2022

2123
file_path: str
2224
mne_type_map: dict | None
2325
selected_stream_indices: List[int]
24-
map_id_to_idx: dict
26+
map_id_to_idx: dict # Stream identifier to the index in our list
2527
verbose: bool
2628

2729
def __init__(self,
@@ -32,7 +34,7 @@ def __init__(self,
3234
scale: float | str | None = None,
3335
verbose: bool = False,
3436
convert_to_mne: bool = True,
35-
):
37+
):
3638

3739
self.file_path = file_path
3840
self.scale = scale
@@ -47,13 +49,19 @@ def __init__(self,
4749

4850
self.available_streams = [XDFStream(stream, mne_type_map=mne_type_map) for stream in streams]
4951

50-
self.map_streams()
52+
self.init_map_streams()
5153

5254
# Prepare the "selected_streams" list
5355
if select_matches is not None:
5456
self.select_streams_by_matches(select_matches)
55-
else:
57+
elif select_type is not None:
5658
self.select_streams_by_type(select_type)
59+
else:
60+
self.select_all_streams()
61+
62+
# Assert that we have found at least one real stream of selected type
63+
if len(self.selected_stream_indices) == 0:
64+
raise ValueError(f"No stream selected. select_type: {select_type}, select_matches: {select_matches}")
5765

5866
if verbose:
5967
print(self)
@@ -63,25 +71,52 @@ def __init__(self,
6371
self.convert_streams_to_mne()
6472

6573
@property
66-
def raws_dict(self):
74+
def selected_streams(self):
75+
"""List of selected streams, given the select_match or select_type"""
76+
return [self.available_streams[idx] for idx in self.selected_stream_indices]
77+
78+
@property
79+
def selected_signal_streams(self):
80+
"""List of selected streams that are signal streams (srate>0), given the select_match or select_type"""
81+
return [self.available_streams[idx] for idx in self.selected_stream_indices if self.available_streams[idx].is_mne_raw_compatible]
82+
83+
@property
84+
def selected_markers_streams(self):
85+
"""List of selected streams that are markers streams (srate==0), given the select_match or select_type"""
86+
return [self.available_streams[idx] for idx in self.selected_stream_indices if self.available_streams[idx].is_mne_annotations_compatible]
87+
88+
@property
89+
def mne_raws_dict(self):
90+
"""Dictionary of mne.io.RawArray object for selected streams, by stream name"""
6791
ret = dict()
68-
for stream in self.selected_data_streams:
69-
ret[stream.name] = stream.raw
92+
for stream in self.selected_signal_streams:
93+
ret[stream.name] = stream.mne_raw
7094
return ret
7195

7296
@property
73-
def raws(self):
74-
return [stream.raw for stream in self.selected_data_streams]
97+
def mne_raws(self):
98+
"""List of mne.io.RawArray object for selected streams"""
99+
return [stream.mne_raw for stream in self.selected_signal_streams]
75100

76101
@property
77102
def markers_dict(self) -> Dict[str, Markers]:
103+
"""
104+
Dictionary of Markers object from selected streams that are not signals (srate==0), by stream name
105+
106+
Markers can be converted to mne.Annotations for signal streams
107+
"""
78108
ret = dict()
79109
for stream in self.selected_markers_streams:
80110
ret[stream.name] = stream.markers
81111
return ret
82112

83113
@property
84114
def markers(self) -> Markers:
115+
"""
116+
Merge list of all Markers from selected streams that are not signals (srate==0)
117+
118+
Markers can be converted to mne.Annotations for signal streams
119+
"""
85120
markers = None
86121
for stream in self.selected_markers_streams:
87122
if markers is None:
@@ -90,75 +125,56 @@ def markers(self) -> Markers:
90125
markers += stream.markers
91126
return markers
92127

93-
@property
94-
def selected_streams(self):
95-
return [self.available_streams[idx] for idx in self.selected_stream_indices]
96-
97-
@property
98-
def selected_data_streams(self):
99-
return [self.available_streams[idx] for idx in self.selected_stream_indices if self.available_streams[idx].is_mne_raw_compatible]
100-
101-
@property
102-
def selected_markers_streams(self):
103-
return [self.available_streams[idx] for idx in self.selected_stream_indices if self.available_streams[idx].is_mne_annotations_compatible]
104-
105-
@property
106-
def selected_stream_names(self):
107-
return [stream.name for stream in self.selected_streams]
108-
109-
@property
110-
def selected_data_stream_names(self):
111-
return [stream.name for stream in self.selected_data_streams]
112-
113-
@property
114-
def selected_markers_stream_names(self):
115-
return [stream.name for stream in self.selected_markers_streams]
116-
117-
def get_streams_for_type(self, stream_type: str):
118-
return [stream for stream in self.available_streams if stream.type == stream_type]
119-
120128
def get_stream_indices_for_type(self, stream_type: str):
129+
"""For a given type of stream, get the list of index in the available streams which are of this type"""
121130
return [idx for idx, stream in enumerate(self.available_streams) if stream.type == stream_type]
122131

123132
def get_stream_ids_for_type(self, stream_type: str):
133+
"""For a given type of stream, get the XDF identifiers in the available streams which are of this type"""
124134
return [stream.id for stream in self.available_streams if stream.type == stream_type]
125135

126-
def map_streams(self):
127-
# create mapping between stream indentifiers and indices in our available_streams list
128-
# id is the stream identifier
129-
# idx is the index of the stream in our list "available_streams"
136+
def init_map_streams(self):
137+
"""
138+
Create mapping between stream indentifiers and indices in our available_streams list
139+
140+
'id' is the stream identifier
141+
142+
'idx' is the index of the stream in our list "available_streams"
143+
"""
130144
for idx, stream in enumerate(self.available_streams):
131145
self.map_id_to_idx[stream.id] = idx
132146

133147

148+
def select_all_streams(self) -> list:
149+
"""
150+
Find in the available streams loaded from the XDF file all the streams that can be converted to MNE
151+
152+
Subsequent calls to class methods will only apply to the selected streams
153+
"""
154+
self.selected_stream_indices = [idx for idx, stream in enumerate(self.available_streams) if stream.is_mne_compatible]
155+
156+
134157
def select_streams_by_type(self, stream_type: str) -> list:
135158
"""
136-
Read the XDF file to find & store the XDF stream's indexes that match the `type` (e.g., "EEG").
159+
Find in the available streams loaded from the XDF file the streams that are of a specific type.
160+
161+
Subsequent calls to class methods will only apply to the selected streams
137162
138-
Arguments:
163+
Parameters:
139164
type: The string (e.g., "EEG", "video") that will be matched to XDF stream's `type` to find their indexes.
140165
"""
141166

142167
if self.verbose:
143168
print(f"Looking for streams of type '{stream_type}'")
144169

145-
if stream_type is not None:
146-
self.selected_stream_indices = self.get_stream_indices_for_type(stream_type)
147-
else:
148-
# Use all the mne raw compatible
149-
# TODO this if is complicated
150-
self.selected_stream_indices = [idx for idx, stream in enumerate(self.available_streams) if stream.is_mne_compatible]
151-
152-
# Assert that we have found at least one real stream of selected type
153-
if len(self.selected_stream_indices) == 0:
154-
raise ValueError(f"No stream of type '{stream_type}' were found in this XDF file")
170+
self.selected_stream_indices = self.get_stream_indices_for_type(stream_type)
155171

156172
def select_streams_by_matches(self, keyword_matches: list):
157173
"""
158174
Interpret the query made by the user (a list of indexes, or `str` that matches
159175
streams' name) into a list containing the indexes within the XDF file.
160176
161-
Arguments:
177+
Parameters:
162178
idx: List containing the index that the user is trying to convert.
163179
"""
164180
for keyword_match in keyword_matches:
@@ -181,59 +197,74 @@ def select_streams_by_matches(self, keyword_matches: list):
181197

182198
def convert_streams_to_mne(self):
183199
"""
184-
A function that centralizes the pipeline for creating a dictionary containing converted
185-
XDF stream into `mne.Raw`.
200+
Create mne.io.RawArray objects from every signal streams, and add all markers as mne.Annotations to the mne.io.RawArray objects
186201
187-
Note:
188-
The returned dictionary has the name of the stream as a key and the `mne.Raw` object as the value.
202+
mne.io.RawArray objects are then available using obj.mne_raws or obj.mne_raws_dict
189203
"""
190204

191205
# Find if all the stream have unique names (true if any stream name is duplicated)
192-
names = self.selected_stream_names
206+
names = [stream.name for stream in self.selected_streams]
193207

194208
for stream in self.selected_streams:
195209
if self.verbose:
196-
print(f'Converting {stream.name}')
210+
print(f'Converting {stream.name} to MNE')
197211

198212
has_duplicate_names = names.count(stream.name) > 1
199213
if has_duplicate_names:
200214
warnings.warn("Multiple streams have the same name. Adding original stream_id as suffixes to the generated raws")
201215

202-
stream.convert_to_mne(self.scale, append_stream_id=has_duplicate_names)
216+
stream.convert_to_mne_raw(self.scale, append_stream_id=has_duplicate_names)
203217

204218
# add annotations from markers streams to data streams
205-
for data_stream in self.selected_data_streams:
219+
for data_stream in self.selected_signal_streams:
206220
if self.markers is not None:
207-
data_stream.raw.set_annotations(self.markers.as_mne_annotations_for_stream(data_stream.stream))
221+
data_stream.mne_raw.set_annotations(self.markers.as_mne_annotations(data_stream.reference_time))
208222

209223
if self.verbose:
210-
print("Convertion done.")
224+
print("All convertion to MNE done.")
211225

212-
def save_to_fif_files(self, dir_path):
213-
return [stream.save_to_fif_file(dir_path) for stream in self.selected_data_streams]
214-
215226
def rename_channels(self, new_names):
216-
return [stream.rename_channels(new_names) for stream in self.selected_data_streams]
227+
"""
228+
Set the name of all the channels for every signal streams. Useful when they were not correctly loaded (or not present) from the XDF file
229+
230+
Parameters:
231+
new_names (List[str]): The list of new names to set
232+
"""
233+
for stream in self.selected_signal_streams:
234+
stream.rename_channels(new_names)
217235

218236
def set_montage(self, montage):
219237
"""
220238
Set the montage of the raw(s) using a custom mne montage label, or the path to a dig.montage file.
221239
222-
Arguments:
240+
Parameters:
223241
self: The instance of the class.
224242
montage: A path to a local Dig montage or a mne standard montage.
225243
"""
226244
if self.verbose or True:
227-
print(f"Setting '{montage}' as the montage for streams: {','.join(self.selected_data_stream_names)}")
245+
names = [stream.name for stream in self.selected_signal_streams]
246+
print(f"Setting '{montage}' as the montage for streams: {','.join(names)}")
228247

229248

230-
for stream in self.selected_data_streams:
249+
for stream in self.selected_signal_streams:
231250
try:
232251
stream.set_montage(montage)
233252
except ValueError as e:
234253
warnings.warn(f"Invalid montage given to mne.set_montage(): {montage}")
235254
raise e
236255

256+
def save_to_fif_files(self, dir_path):
257+
"""
258+
Save all the mne.io.RawArray objects as .fif files
259+
260+
Parameters:
261+
dir_path (str): Relative or absolute path of the folder where to save the .fif files
262+
263+
Returns:
264+
List[str]: The list of file names that have been created
265+
"""
266+
return [stream.save_to_fif_file(dir_path) for stream in self.selected_signal_streams]
267+
237268
def __str__(self):
238269
available_streams_str = "\n ".join([str(stream) for stream in self.available_streams])
239270
selected_streams_str = ",".join([stream.name for stream in self.selected_streams])

0 commit comments

Comments
 (0)